A devtool that visualizes a Kovo app's dataflow graph — select any node and trace the queries in and mutations out — and serves the same graph cards to agents over MCP. It is itself a Kovo app, dogfooding the framework on its own tooling (SPEC §5.3: agents consume the same artifact humans read).

Runnable Kovo example app under `examples/devtool`. The authored source below shows what it demonstrates — the components, queries, mutations, and derived optimism that drive it (lowered IR / generated components are artifacts, not authored; SPEC §5.2).

```ts title="examples/devtool/src/app-shell.ts"
// Thin consumer of @kovojs/devtool. It wires three sibling example apps' own
// emitted graphs into the reusable devtool; all the logic (graph derivation,
// rendering, MCP, mount) lives in the package. This is what any host does to
// inspect its own app — read its graph.json, hand it to createDevtoolApp.
import '@kovojs/server/runtime-bootstrap';

import { existsSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

import { buildBundle } from '@kovojs/devtool';
import { createDevtoolApp } from '@kovojs/devtool/app';

const HERE = dirname(fileURLToPath(import.meta.url));
const EXAMPLES = join(HERE, '..', '..');

const APPS = [
  { app: 'commerce', label: 'Commerce', blurb: 'cart · products · orders' },
  { app: 'crm', label: 'CRM', blurb: 'contacts · deals · pipeline' },
  { app: 'stackoverflow', label: 'Stack Overflow', blurb: 'questions · answers · votes' },
];

const bundles = APPS.map((a) => {
  const graphPath = join(EXAMPLES, a.app, 'src', 'generated', 'graph.json');
  if (!existsSync(graphPath)) {
    return JSON.parse(readFileSync(join(HERE, '..', 'data', `${a.app}.json`), 'utf8'));
  }
  const graph = JSON.parse(readFileSync(graphPath, 'utf8'));
  return buildBundle({
    app: a.app,
    label: a.label,
    blurb: a.blurb,
    graph,
    srcRoot: join(EXAMPLES, a.app, 'src'),
  });
});

const devtool = createDevtoolApp({ bundles });

export const app = devtool.app;
export const manifest = devtool.manifest;
export const requestHandler = devtool.requestHandler;
export const runtimeFrames = devtool.runtimeFrames;
export const nodeHandler = devtool.nodeHandler;
export default app;
```
```js title="packages/devtool/src/graph-model.mjs"
// @kovojs/devtool — DataflowGraph model (Phase 0, shared bedrock)
//
// Pure derivation over the compiler's existing `KovoExplainInput`
// (generated/graph.json). No source analysis, no fs — this only *indexes* facts
// the framework already proves (SPEC §11.1 touch-sets ⋈ §10.2 read-sets) into a
// navigable shape. Imported by both the node bundle script and the browser UI so
// the two surfaces can never diverge (SPEC §5.3 — one artifact, two renderers).
import {
  arrayAppend,
  arrayFilter,
  arrayLength,
  arrayMap,
  arrayReduce,
  arraySlice,
  arraySort,
  arrayValue,
  createMap,
  createSet,
  freeze,
  isSafeInteger,
  joinStrings,
  mapGet,
  mapHas,
  mapSet,
  numberLog,
  setAdd,
  setHas,
  stringCharCodeAt,
  stringSlice,
  stringStartsWith,
} from './output-security.mjs';

/**
 * @typedef {'agent'|'tool'|'task'|'mutation'|'domain'|'query'|'component'|'handler'|'trigger'|'derive'|'binding-position'|'page'|'diagnostic'} NodeKind
 * @typedef {'uses'|'invokes'|'dispatches'|'reads'|'schedules'|'writes'|'backs'|'feeds'|'emits'|'renders'|'handles'|'triggers'|'derives'|'owns'|'updates'} EdgeKind
 */

/** Left→right dataflow lanes. A write propagates rightward to the UI. */
export const LANES = freeze([
  'agent',
  'tool',
  'task',
  'mutation',
  'domain',
  'query',
  'component',
  'page',
  'diagnostic',
]);

export const KIND_META = freeze({
  agent: freeze({
    accent: '#fb7185',
    blurb: 'model mediation',
    glyph: '◉',
    label: 'Agents',
  }),
  tool: freeze({
    accent: '#f472b6',
    blurb: 'bounded actions',
    glyph: '⌁',
    label: 'Tools',
  }),
  task: freeze({
    accent: '#fbbf24',
    blurb: 'durable work',
    glyph: '◴',
    label: 'Tasks',
  }),
  mutation: freeze({
    accent: '#f5a623',
    blurb: 'typed writes',
    glyph: '⚡',
    label: 'Mutations',
  }),
  domain: freeze({
    accent: '#34d399',
    blurb: 'invalidation units',
    glyph: '◆',
    label: 'Domains',
  }),
  query: freeze({ accent: '#38bdf8', blurb: 'typed reads', glyph: '◎', label: 'Queries' }),
  component: freeze({
    accent: '#a78bfa',
    blurb: 'render + handlers',
    glyph: '▢',
    label: 'Components',
  }),
  handler: freeze({
    accent: '#c792ea',
    blurb: 'browser behavior',
    glyph: '↯',
    label: 'Handlers',
  }),
  trigger: freeze({
    accent: '#c792ea',
    blurb: 'execution timing',
    glyph: '◷',
    label: 'Triggers',
  }),
  derive: freeze({
    accent: '#c792ea',
    blurb: 'computed bindings',
    glyph: 'ƒ',
    label: 'Derives',
  }),
  'binding-position': freeze({
    accent: '#c792ea',
    blurb: 'refresh coverage',
    glyph: '⌖',
    label: 'Bindings',
  }),
  page: freeze({ accent: '#94a3b8', blurb: 'routes', glyph: '◧', label: 'Pages' }),
  diagnostic: freeze({
    accent: '#ff6b6b',
    blurb: 'actionable findings',
    glyph: '!',
    label: 'Diagnostics',
  }),
});

const id = (kind, name) => `${kind}:${name}`;

/** Keep component-local detail nodes in the component lane rather than multiplying columns. */
export function laneForKind(kind) {
  return kind === 'handler' ||
    kind === 'trigger' ||
    kind === 'derive' ||
    kind === 'binding-position'
    ? 'component'
    : kind;
}

/**
 * Build the traversable dataflow graph from a raw KovoExplainInput object.
 * @param {any} raw parsed generated/graph.json
 * @returns {{nodes: any[], edges: any[], byId: Record<string, any>, index: any}}
 */
export function buildDataflowGraph(raw) {
  /** @type {Map<string, any>} */
  const nodes = new Map();
  const edges = [];

  const ensure = (kind, name, label, data = {}, anchor) => {
    const nid = id(kind, name);
    if (!nodes.has(nid)) {
      nodes.set(nid, {
        id: nid,
        kind,
        name,
        label: label ?? name,
        data,
        ...(anchor === undefined ? {} : { anchor }),
      });
    } else {
      Object.assign(nodes.get(nid).data, data);
      if (anchor !== undefined && nodes.get(nid).anchor === undefined) {
        nodes.get(nid).anchor = anchor;
      }
    }
    return nodes.get(nid);
  };

  const link = (from, to, kind, data = {}, anchor) => {
    if (!from || !to) return;
    const occurrenceIdentity =
      anchor !== undefined &&
      (kind === 'uses' ||
        kind === 'invokes' ||
        kind === 'dispatches' ||
        kind === 'reads' ||
        kind === 'schedules')
        ? `@${anchor.file}:${anchor.start}:${anchor.end}`
        : '';
    edges.push({
      id: `${from.id}->${to.id}:${kind}${occurrenceIdentity}`,
      from: from.id,
      to: to.id,
      kind,
      data,
      ...(anchor === undefined ? {} : { anchor }),
    });
  };

  // --- domains (union of every mention; exact declarations win over usage fallbacks) ---
  const domainAnchors = createMap();
  for (const domain of raw.domains ?? []) {
    if (domain?.name && domain?.source && !mapHas(domainAnchors, domain.name)) {
      mapSet(domainAnchors, domain.name, domain.source);
    }
  }
  const domainNode = (d) => ensure('domain', d, d, {}, mapGet(domainAnchors, d));

  // --- mutations ---
  const optByMutation = groupBy(raw.optimistic ?? [], (o) => o.mutation);
  for (const m of raw.mutations ?? []) {
    const writes = m.writes ?? m.invalidates ?? [];
    const node = ensure(
      'mutation',
      m.key,
      m.key,
      {
        guards: m.guards ?? [],
        writes,
        invalidates: m.invalidates ?? writes,
        inputFields: m.inputFields ?? [],
        session: m.session,
        optimistic: optByMutation.get(m.key) ?? [],
        touch: raw.touchGraph?.[m.key] ?? findTouchByDomains(raw.touchGraph, writes),
      },
      m.source,
    );
    for (const d of writes) link(node, domainNode(d), 'writes', {}, m.source);
  }

  // --- queries (query → backing domains) ---
  for (const q of raw.queries ?? []) {
    const node = ensure(
      'query',
      q.query,
      q.query,
      {
        domains: q.domains ?? [],
        guards: q.guards ?? [],
      },
      q.source,
    );
    for (const d of q.domains ?? []) link(domainNode(d), node, 'backs', {}, q.source);
  }

  // --- components (queries in, mutations out via forms) ---
  for (const c of raw.components ?? []) {
    const label = c.exportName ?? leaf(c.name);
    const node = ensure(
      'component',
      c.name,
      label,
      {
        domName: c.domName,
        exportName: c.exportName,
        queries: c.queries ?? [],
        fragments: c.fragments ?? [],
        mutationForms: c.mutationForms ?? [],
        handlers: c.handlers ?? [],
      },
      c.source,
    );
    for (const qn of c.queries ?? []) {
      const qnode = nodes.get(id('query', qn));
      if (qnode) link(qnode, node, 'feeds', {}, c.source); // query → component (data in)
    }
    for (const mf of c.mutationForms ?? []) {
      const mnode = nodes.get(id('mutation', mf.mutation));
      if (mnode)
        link(node, mnode, 'emits', { slot: mf.slot, fields: mf.fields }, mf.source ?? c.source); // component → mutation (action out)
    }
    for (const handler of c.handlers ?? []) {
      const handlerNode = ensure(
        'handler',
        `${c.name}:${handler.exportName}`,
        handler.event,
        { component: c.name, exportName: handler.exportName, ref: handler.ref },
        handler.source,
      );
      link(node, handlerNode, 'handles', {}, handler.generatedFrom ?? handler.source ?? c.source);
    }
    for (const trigger of c.triggers ?? []) {
      const triggerNode = ensure(
        'trigger',
        `${c.name}:${trigger.exportName}`,
        trigger.trigger,
        { component: c.name, exportName: trigger.exportName, ref: trigger.ref },
        trigger.source,
      );
      link(node, triggerNode, 'triggers', {}, trigger.generatedFrom ?? trigger.source ?? c.source);
    }
    for (const derive of c.derives ?? []) {
      const deriveNode = ensure(
        'derive',
        `${c.name}:${derive.name}:${derive.target}`,
        derive.name,
        { component: c.name, inputs: derive.inputs, ref: derive.ref, target: derive.target },
        derive.source,
      );
      link(deriveNode, node, 'derives', {}, derive.generatedFrom ?? derive.source ?? c.source);
    }
  }

  // --- update-coverage binding positions (compiler facts; no source rediscovery) ---
  let coverageIndex = 0;
  for (const coverage of raw.updateCoverage ?? []) {
    const component = componentNodeForCoverage(nodes, coverage.component);
    if (!component || !coverage.sourceAnchor) continue;
    const binding = ensure(
      'binding-position',
      `${component.name}:${coverage.sourceAnchor.file}:${coverage.sourceAnchor.start}:${coverageIndex}`,
      `${coverage.status} · ${coverage.query}`,
      {
        component: component.name,
        compilerComponentName: coverage.component,
        detail: coverage.detail,
        position: coverage.position,
        query: coverage.query,
        source: coverage.source ?? 'query',
        status: coverage.status,
      },
      coverage.sourceAnchor,
    );
    link(component, binding, 'owns', {}, coverage.sourceAnchor);
    const query = queryNodeForCoverage(nodes, coverage.query);
    if (query) {
      link(
        query,
        binding,
        'updates',
        { detail: coverage.detail, position: coverage.position, status: coverage.status },
        coverage.sourceAnchor,
      );
    }
    coverageIndex += 1;
  }

  // --- capability-bounded agents and their exact tool/mutation bindings ---
  for (const agent of raw.agents ?? []) {
    const agentNode = ensure(
      'agent',
      agent.name,
      agent.name,
      { modelOperations: agent.modelOperations ?? [] },
      agent.source,
    );
    for (const tool of agent.tools ?? []) {
      const toolNode = ensure(
        'tool',
        `${agent.name}:${tool.name}`,
        tool.name,
        {
          agent: agent.name,
          minimumIntegrity: tool.minimumIntegrity,
          mutation: tool.mutation,
          operations: tool.operations ?? [],
          resultIntegrity: tool.resultIntegrity,
        },
        tool.source,
      );
      link(agentNode, toolNode, 'uses', {}, tool.bindingSource);
      link(
        toolNode,
        nodes.get(id('mutation', tool.mutation)),
        'invokes',
        { mutation: tool.mutation },
        tool.mutationSource,
      );
    }
  }

  // --- durable tasks and exact task/query/mutation composition edges ---
  for (const task of raw.tasks ?? []) {
    ensure(
      'task',
      task.key,
      task.key,
      {
        composition: task.composition ?? [],
        cron: task.cron,
        runMutations: task.runMutations ?? [],
        runQueries: task.runQueries ?? [],
        schedules: task.schedules ?? [],
      },
      task.source,
    );
  }
  for (const task of raw.tasks ?? []) {
    const taskNode = nodes.get(id('task', task.key));
    for (const edge of task.composition ?? []) {
      if (edge.kind === 'run-mutation') {
        link(
          taskNode,
          nodes.get(id('mutation', edge.target)),
          'dispatches',
          { target: edge.target },
          edge.source,
        );
      } else if (edge.kind === 'run-query') {
        link(
          taskNode,
          nodes.get(id('query', edge.target)),
          'reads',
          { target: edge.target },
          edge.source,
        );
      } else if (edge.kind === 'schedule') {
        link(
          taskNode,
          nodes.get(id('task', edge.target)),
          'schedules',
          { target: edge.target },
          edge.source,
        );
      }
    }
  }

  // --- pages (renders components, loads queries) ---
  for (const p of raw.pages ?? []) {
    const node = ensure(
      'page',
      p.route,
      p.route,
      {
        meta: p.meta,
        prefetch: p.prefetch,
        guards: p.guards ?? [],
        layouts: p.layouts ?? [],
      },
      p.source,
    );
    const compExports = [];
    for (const seg of p.navigationSegments ?? [])
      for (const c of seg.components ?? []) compExports.push(c);
    for (const exp of compExports) {
      const match = [...nodes.values()].find(
        (n) => n.kind === 'component' && (n.data.exportName === exp || n.label === exp),
      );
      if (match) link(node, match, 'renders', {}, p.source);
    }
  }

  const byId = Object.fromEntries(nodes);
  const list = [...nodes.values()];

  // --- reverse indices: the traversal the user asked for ---
  const index = buildIndex(list, edges, byId);

  return { nodes: list, edges, byId, index };
}

function componentNodeForCoverage(nodes, name) {
  const exact = nodes.get(id('component', name));
  if (exact) return exact;
  for (const node of nodes.values()) {
    if (node.kind === 'component' && (node.data.exportName === name || node.label === name)) {
      return node;
    }
  }
  return undefined;
}

function queryNodeForCoverage(nodes, path) {
  let selected;
  for (const node of nodes.values()) {
    if (
      node.kind !== 'query' ||
      (path !== node.name && !stringStartsWith(path, `${node.name}.`)) ||
      (selected && selected.name.length >= node.name.length)
    ) {
      continue;
    }
    selected = node;
  }
  return selected;
}

function buildIndex(nodes, edges, byId) {
  const out = new Map(); // id -> edges leaving
  const inc = new Map(); // id -> edges entering
  for (const n of nodes) {
    out.set(n.id, []);
    inc.set(n.id, []);
  }
  for (const e of edges) {
    out.get(e.from)?.push(e);
    inc.get(e.to)?.push(e);
  }

  /** queries-in for a component (the `feeds` edges) + their backing domains + mutations that invalidate them. */
  const componentInflow = (cid) => {
    const queries = (inc.get(cid) ?? []).filter((e) => e.kind === 'feeds').map((e) => byId[e.from]);
    const detail = queries.map((q) => {
      const domains = (inc.get(q.id) ?? [])
        .filter((e) => e.kind === 'backs')
        .map((e) => byId[e.from]);
      const invalidators = nodes
        .filter(
          (n) => n.kind === 'mutation' && n.data.writes.some((d) => q.data.domains.includes(d)),
        )
        .map((m) => ({ mutation: m, status: optStatus(m, q.name) }));
      return { query: q, domains, invalidators };
    });
    return detail;
  };

  /** mutations-out for a component (forms) + each mutation's full downstream effect. */
  const componentOutflow = (cid) => {
    const mutations = (out.get(cid) ?? [])
      .filter((e) => e.kind === 'emits')
      .map((e) => byId[e.from === cid ? e.to : e.from]);
    return mutations.map((m) => ({ mutation: m, effects: mutationEffects(m) }));
  };

  /** every query a mutation invalidates and the components that read it. */
  const mutationEffects = (m) => {
    const queries = nodes.filter(
      (n) => n.kind === 'query' && n.data.domains.some((d) => m.data.writes.includes(d)),
    );
    return queries.map((q) => ({
      query: q,
      status: optStatus(m, q.name),
      components: (out.get(q.id) ?? []).filter((e) => e.kind === 'feeds').map((e) => byId[e.to]),
    }));
  };

  function optStatus(m, queryName) {
    const o = (m.data.optimistic ?? []).find((x) => x.query === queryName);
    return o ? { status: o.status, derivation: o.derivation } : null;
  }

  /** all node ids reachable along the dataflow when one node is selected (for highlight). */
  const traceFrom = (nid) => traceAdjacency(out, inc, nid);

  return { out, inc, componentInflow, componentOutflow, mutationEffects, optStatus, traceFrom };
}

/** @internal Iterative graph trace shared by the renderer and model index. */
export function traceGraph(nodes, edges, nid) {
  const out = createMap();
  const inc = createMap();
  for (let index = 0; index < arrayLength(nodes, 'devtool graph nodes'); index += 1) {
    const node = arrayValue(nodes, index, 'devtool graph nodes');
    mapSet(out, node.id, []);
    mapSet(inc, node.id, []);
  }
  for (let index = 0; index < arrayLength(edges, 'devtool graph edges'); index += 1) {
    const edge = arrayValue(edges, index, 'devtool graph edges');
    const outgoing = mapGet(out, edge.from);
    const incoming = mapGet(inc, edge.to);
    if (outgoing) arrayAppend(outgoing, edge, 'devtool outgoing edges');
    if (incoming) arrayAppend(incoming, edge, 'devtool incoming edges');
  }
  return traceAdjacency(out, inc, nid);
}

function traceAdjacency(out, inc, nid) {
  const nodes = createSet();
  const edges = createSet();
  setAdd(nodes, nid);

  const walk = (adjacency, direction) => {
    const visited = createSet();
    const work = [nid];
    setAdd(visited, nid);
    for (let cursor = 0; cursor < arrayLength(work, 'devtool trace worklist'); cursor += 1) {
      const current = arrayValue(work, cursor, 'devtool trace worklist');
      const candidates = mapGet(adjacency, current) ?? [];
      for (
        let edgeIndex = 0;
        edgeIndex < arrayLength(candidates, 'devtool trace edges');
        edgeIndex += 1
      ) {
        const edge = arrayValue(candidates, edgeIndex, 'devtool trace edges');
        const next = direction === 'down' ? edge.to : edge.from;
        setAdd(edges, edge.id);
        setAdd(nodes, next);
        if (!setHas(visited, next)) {
          setAdd(visited, next);
          arrayAppend(work, next, 'devtool trace worklist');
        }
      }
    }
  };
  walk(out, 'down');
  walk(inc, 'up');
  return { nodes, edges };
}

// ---------- BM25 retrieval (the MCP tool's ranking, also powers UI search) ----------
// Deterministic, explainable lexical ranking over node "cards". SPEC values
// stable/diffable/legible output, so BM25 (reproducible, matched-terms auditable)
// fits where an embedding model would not.

export function buildBm25(nodes) {
  const docs = arrayMap(
    nodes,
    (node) => ({ id: node.id, terms: tokenize(cardText(node)) }),
    'devtool BM25 nodes',
  );
  const N = arrayLength(docs, 'devtool BM25 documents');
  const df = createMap();
  for (let docIndex = 0; docIndex < N; docIndex += 1) {
    const terms = uniqueStrings(
      arrayValue(docs, docIndex, 'devtool BM25 documents').terms,
      'devtool BM25 document terms',
    );
    for (let termIndex = 0; termIndex < arrayLength(terms, 'devtool BM25 terms'); termIndex += 1) {
      const term = arrayValue(terms, termIndex, 'devtool BM25 terms');
      mapSet(df, term, (mapGet(df, term) ?? 0) + 1);
    }
  }
  const avgdl =
    arrayReduce(
      docs,
      (sum, document) => sum + arrayLength(document.terms, 'devtool BM25 document terms'),
      0,
      'devtool BM25 documents',
    ) / (N > 1 ? N : 1);
  const k1 = 1.5,
    b = 0.75;
  const idf = (term) => {
    const frequency = mapGet(df, term) ?? 0;
    return numberLog(1 + (N - frequency + 0.5) / (frequency + 0.5));
  };

  return function search(queryStr, limit = 8) {
    if (!isSafeInteger(limit) || limit < 0) {
      throw new TypeError('Kovo devtool BM25 limit must be a non-negative safe integer.');
    }
    const qterms = tokenize(queryStr);
    const uniqueQueryTerms = uniqueStrings(qterms, 'devtool BM25 query terms');
    const scored = arrayMap(
      docs,
      (document) => {
        const tf = createMap();
        for (
          let termIndex = 0;
          termIndex < arrayLength(document.terms, 'devtool BM25 document terms');
          termIndex += 1
        ) {
          const term = arrayValue(document.terms, termIndex, 'devtool BM25 document terms');
          mapSet(tf, term, (mapGet(tf, term) ?? 0) + 1);
        }
        let score = 0;
        const matched = [];
        for (
          let queryIndex = 0;
          queryIndex < arrayLength(uniqueQueryTerms, 'devtool BM25 query terms');
          queryIndex += 1
        ) {
          const queryTerm = arrayValue(uniqueQueryTerms, queryIndex, 'devtool BM25 query terms');
          const frequency = mapGet(tf, queryTerm) ?? 0;
          if (!frequency) continue;
          arrayAppend(matched, queryTerm, 'devtool BM25 matched terms');
          score +=
            (idf(queryTerm) * (frequency * (k1 + 1))) /
            (frequency +
              k1 *
                (1 - b + b * (arrayLength(document.terms, 'devtool BM25 document terms') / avgdl)));
        }
        return { id: document.id, matched, score };
      },
      'devtool BM25 documents',
    );
    return arraySlice(
      arraySort(
        arrayFilter(scored, (result) => result.score > 0, 'devtool BM25 scores'),
        (left, right) => right.score - left.score,
        'devtool BM25 matches',
      ),
      0,
      limit,
      'devtool BM25 sorted matches',
    );
  };
}

/** Render a node to the retrievable "card" text — its traced neighborhood. */
function cardText(n) {
  const parts = [n.kind, n.name, n.label];
  const d = n.data;
  if (n.kind === 'component') {
    appendCardParts(parts, ['component', d.domName]);
    appendCardParts(parts, d.queries ?? []);
    const forms = d.mutationForms ?? [];
    for (let index = 0; index < arrayLength(forms, 'devtool mutation forms'); index += 1) {
      const form = arrayValue(forms, index, 'devtool mutation forms');
      appendCardParts(parts, ['mutation', 'form', form.mutation]);
      appendCardParts(parts, form.fields ?? []);
    }
  } else if (n.kind === 'query') {
    appendCardParts(parts, ['query', 'read']);
    appendCardParts(parts, d.domains ?? []);
    appendCardParts(parts, d.guards ?? []);
  } else if (n.kind === 'mutation') {
    appendCardParts(parts, ['mutation', 'write']);
    appendCardParts(parts, d.writes ?? []);
    appendCardParts(parts, d.inputFields ?? []);
    appendCardParts(parts, d.guards ?? []);
    const optimistic = d.optimistic ?? [];
    for (let index = 0; index < arrayLength(optimistic, 'devtool optimistic facts'); index += 1) {
      const fact = arrayValue(optimistic, index, 'devtool optimistic facts');
      appendCardParts(parts, [fact.query, fact.status, fact.derivation?.reason?.code ?? '']);
    }
  } else if (n.kind === 'domain') {
    arrayAppend(parts, 'domain', 'devtool card parts');
  } else if (n.kind === 'agent') {
    appendCardParts(parts, ['agent', 'model']);
    const operations = d.modelOperations ?? [];
    for (let index = 0; index < arrayLength(operations, 'devtool agent operations'); index += 1) {
      appendCardParts(parts, [
        arrayValue(operations, index, 'devtool agent operations').kind ?? '',
      ]);
    }
  } else if (n.kind === 'tool') {
    appendCardParts(parts, [
      'tool',
      d.agent ?? '',
      d.mutation ?? '',
      d.minimumIntegrity ?? '',
      d.resultIntegrity ?? '',
    ]);
  } else if (n.kind === 'task') {
    appendCardParts(parts, ['task', d.cron ?? '']);
    appendCardParts(parts, d.runMutations ?? []);
    appendCardParts(parts, d.runQueries ?? []);
    appendCardParts(parts, d.schedules ?? []);
  } else if (n.kind === 'diagnostic') {
    appendCardParts(parts, [
      'diagnostic',
      d.code ?? '',
      d.category ?? '',
      d.severity ?? '',
      d.message ?? '',
      d.help ?? '',
      d.source?.file ?? '',
    ]);
  } else if (n.kind === 'page') {
    appendCardParts(parts, ['page', 'route', d.meta?.title ?? '']);
  }
  return joinStrings(
    arrayFilter(parts, (part) => typeof part === 'string' && part.length > 0, 'devtool card parts'),
    ' ',
    'devtool card text',
  );
}

function tokenize(s) {
  if (typeof s !== 'string') throw new TypeError('Kovo devtool BM25 text must be a string.');
  const terms = [];
  let current = '';
  let previousWasLowerOrDigit = false;
  const flush = () => {
    if (current.length > 1) arrayAppend(terms, current, 'devtool BM25 tokens');
    current = '';
  };
  for (let index = 0; index < s.length; index += 1) {
    const code = stringCharCodeAt(s, index);
    const uppercase = code >= 65 && code <= 90;
    const lowercase = code >= 97 && code <= 122;
    const digit = code >= 48 && code <= 57;
    if (uppercase && previousWasLowerOrDigit) flush();
    if (uppercase) current += stringSlice('abcdefghijklmnopqrstuvwxyz', code - 65, code - 64);
    else if (lowercase || digit) current += stringSlice(s, index, index + 1);
    else flush();
    previousWasLowerOrDigit = lowercase || digit;
  }
  flush();
  return terms;
}

function appendCardParts(target, values) {
  for (let index = 0; index < arrayLength(values, 'devtool card part values'); index += 1) {
    const value = arrayValue(values, index, 'devtool card part values');
    if (typeof value !== 'string') throw new TypeError('Kovo devtool card parts must be strings.');
    arrayAppend(target, value, 'devtool card parts');
  }
}

function uniqueStrings(values, label) {
  const seen = createMap();
  const unique = [];
  for (let index = 0; index < arrayLength(values, label); index += 1) {
    const value = arrayValue(values, index, label);
    if (typeof value !== 'string') throw new TypeError(`${label}[${index}] must be a string.`);
    if (mapHas(seen, value)) continue;
    mapSet(seen, value, true);
    arrayAppend(unique, value, `${label} unique values`);
  }
  return unique;
}

// ---------- helpers ----------
function groupBy(arr, keyFn) {
  const m = new Map();
  for (const x of arr) {
    const k = keyFn(x);
    if (!m.has(k)) m.set(k, []);
    m.get(k).push(x);
  }
  return m;
}
function leaf(path) {
  return String(path).split('/').pop();
}
function findTouchByDomains(touchGraph, domains) {
  if (!touchGraph) return null;
  for (const [, entry] of Object.entries(touchGraph)) {
    if ((entry.touches ?? []).some((t) => domains.includes(t.domain))) return entry;
  }
  return null;
}
```
```js title="packages/devtool/src/cards.mjs"
// Graph "cards" — the shared, format-neutral fact bundle for one node.
//
// SPEC §5.3: "agents consume the same artifact humans read." This module is that
// artifact, as structured data. The visual inspector renders a card to HTML; the
// MCP `kovo_explain` tool renders the same card to stable text + returns it as
// structuredContent. Both derive from one `buildCard`, so the two surfaces cannot
// drift. The `invalidates` relation is the SPEC §11.1 touch-set ⋈ §10.2 read-set
// join the compiler already proves.

/** @param {any} node @param {any} bundle */
export function buildCard(node, bundle) {
  const byId = new Map(bundle.nodes.map((n) => [n.id, n]));
  const edges = bundle.edges;
  const mutsWriting = (domains) =>
    bundle.nodes.filter(
      (n) => n.kind === 'mutation' && (n.data.writes ?? []).some((d) => domains.includes(d)),
    );
  const optStatus = (m, queryName) => {
    const o = (m.data.optimistic ?? []).find((x) => x.query === queryName);
    return o
      ? {
          status: o.status,
          reason: o.derivation?.status === 'PUNTED' ? o.derivation.reason : undefined,
        }
      : null;
  };
  const ref = (n) => ({ id: n.id, kind: n.kind, label: n.label, name: n.name });

  const card = {
    id: node.id,
    kind: node.kind,
    name: node.name,
    label: node.label,
    endpoint:
      node.kind === 'mutation'
        ? `POST /_m/${node.name}`
        : node.kind === 'query'
          ? `GET /_q/${node.name}`
          : node.kind === 'page'
            ? `GET ${node.name}`
            : undefined,
    guards: node.data.guards ?? [],
    sections: {},
    source: node.source ?? null,
  };
  const S = card.sections;

  if (node.kind === 'component') {
    const queries = edges
      .filter((e) => e.kind === 'feeds' && e.to === node.id)
      .map((e) => byId.get(e.from));
    const mutations = edges
      .filter((e) => e.kind === 'emits' && e.from === node.id)
      .map((e) => byId.get(e.to));
    card.domName = node.data.domName;
    card.fragments = node.data.fragments ?? [];
    S.queriesIn = queries.map((q) => ({
      ...ref(q),
      domains: q.data.domains ?? [],
      invalidators: mutsWriting(q.data.domains ?? []).map((m) => ({
        ...ref(m),
        optimistic: optStatus(m, q.name),
      })),
    }));
    S.mutationsOut = mutations.map((m) => ({ ...ref(m), fields: m.data.inputFields ?? [] }));
  } else if (node.kind === 'mutation') {
    const domains = node.data.writes ?? [];
    const queries = bundle.nodes.filter(
      (n) => n.kind === 'query' && (n.data.domains ?? []).some((d) => domains.includes(d)),
    );
    S.writes = domains.map((d) =>
      ref(byId.get(`domain:${d}`) ?? { id: `domain:${d}`, kind: 'domain', label: d, name: d }),
    );
    S.invalidates = queries.map((q) => ({ ...ref(q), optimistic: optStatus(node, q.name) }));
    S.inputs = node.data.inputFields ?? [];
    S.touchSites = (node.source?.touches ?? []).map((t) => ({
      via: t.via,
      domain: t.domain,
      keys: t.keys ?? null,
      site: t.site,
    }));
  } else if (node.kind === 'query') {
    const domains = node.data.domains ?? [];
    S.reads = domains.map((d) =>
      ref(byId.get(`domain:${d}`) ?? { id: `domain:${d}`, kind: 'domain', label: d, name: d }),
    );
    S.feeds = edges
      .filter((e) => e.kind === 'feeds' && e.from === node.id)
      .map((e) => ref(byId.get(e.to)));
    S.invalidatedBy = mutsWriting(domains).map((m) => ({
      ...ref(m),
      optimistic: optStatus(m, node.name),
    }));
  } else if (node.kind === 'domain') {
    S.backs = bundle.nodes
      .filter((n) => n.kind === 'query' && (n.data.domains ?? []).includes(node.name))
      .map(ref);
    S.writtenBy = bundle.nodes
      .filter((n) => n.kind === 'mutation' && (n.data.writes ?? []).includes(node.name))
      .map(ref);
  } else if (node.kind === 'agent') {
    S.tools = edges
      .filter((edge) => edge.kind === 'uses' && edge.from === node.id)
      .map((edge) => {
        const tool = byId.get(edge.to);
        return {
          ...ref(tool),
          minimumIntegrity: tool.data.minimumIntegrity,
          mutation: tool.data.mutation,
          resultIntegrity: tool.data.resultIntegrity,
        };
      });
    S.modelOperations = node.data.modelOperations ?? [];
  } else if (node.kind === 'tool') {
    S.invokes = edges
      .filter((edge) => edge.kind === 'invokes' && edge.from === node.id)
      .map((edge) => ref(byId.get(edge.to)));
    S.operations = node.data.operations ?? [];
    card.minimumIntegrity = node.data.minimumIntegrity;
    card.resultIntegrity = node.data.resultIntegrity;
  } else if (node.kind === 'task') {
    S.composition = edges
      .filter(
        (edge) =>
          edge.from === node.id &&
          (edge.kind === 'dispatches' || edge.kind === 'reads' || edge.kind === 'schedules'),
      )
      .map((edge) => ({ edge: edge.kind, ...ref(byId.get(edge.to)) }));
    card.cron = node.data.cron;
  } else if (node.kind === 'diagnostic') {
    card.diagnostic = {
      category: node.data.category,
      code: node.data.code,
      help: node.data.help,
      message: node.data.message,
      severity: node.data.severity,
      source: node.data.source,
      version: node.data.version,
    };
  } else if (node.kind === 'page') {
    card.meta = node.data.meta ?? {};
    S.renders = edges
      .filter((e) => e.kind === 'renders' && e.from === node.id)
      .map((e) => ref(byId.get(e.to)));
  }
  return card;
}

const opt = (o) =>
  o ? `[${o.status}${o.reason?.code ? `:${o.reason.code}` : ''}]` : '[no-transform]';
const list = (arr, f) => (arr && arr.length ? arr.map(f).join('\n') : '  (none)');

/** Stable, diffable text rendering — the same facts the inspector shows. */
export function cardToText(card) {
  const L = [`kovo-explain/v1`, `${card.kind.toUpperCase()} ${card.label}`];
  if (card.endpoint) L.push(`endpoint: ${card.endpoint}`);
  if (card.domName) L.push(`dom-name: ${card.domName}`);
  if (card.fragments?.length) L.push(`fragment-targets: ${card.fragments.join(', ')}`);
  if (card.guards?.length) L.push(`guards: ${card.guards.join(', ')}`);
  if (card.diagnostic) {
    const diagnostic = card.diagnostic;
    L.push(`code: ${diagnostic.code}`);
    L.push(`severity: ${diagnostic.severity}`);
    L.push(`category: ${diagnostic.category}`);
    L.push(`message: ${diagnostic.message}`);
    if (diagnostic.help) L.push(`help: ${diagnostic.help}`);
    if (diagnostic.source) {
      L.push(
        `source-span: ${diagnostic.source.file}:${diagnostic.source.start}-${diagnostic.source.end}`,
      );
    }
  }
  const S = card.sections;
  if (S.queriesIn) {
    L.push(`\nQUERIES IN (${S.queriesIn.length})`);
    L.push(
      list(
        S.queriesIn,
        (q) =>
          `  ${q.label}  reads ${q.domains.join(', ') || '—'}` +
          (q.invalidators.length
            ? `\n    refreshed by: ${q.invalidators.map((m) => `${m.label} ${opt(m.optimistic)}`).join(', ')}`
            : ''),
      ),
    );
  }
  if (S.mutationsOut) {
    L.push(`\nMUTATIONS OUT (${S.mutationsOut.length})`);
    L.push(list(S.mutationsOut, (m) => `  ${m.label}  fields: ${m.fields.join(', ') || '—'}`));
  }
  if (S.writes) {
    L.push(`\nWRITES DOMAINS (${S.writes.length})`);
    L.push(list(S.writes, (d) => `  ${d.label}`));
  }
  if (S.invalidates) {
    L.push(`\nINVALIDATES QUERIES (${S.invalidates.length})`);
    L.push(list(S.invalidates, (q) => `  ${q.label}  ${opt(q.optimistic)}`));
  }
  if (S.inputs) L.push(`\nINPUT FIELDS: ${S.inputs.join(', ') || '—'}`);
  if (S.touchSites?.length) {
    L.push(`\nWRITE SITES (touch graph)`);
    L.push(
      list(
        S.touchSites,
        (t) => `  ${t.via} → ${t.domain}${t.keys ? ` (${t.keys})` : ''}  ${t.site}`,
      ),
    );
  }
  if (S.reads) {
    L.push(`\nREADS DOMAINS: ${S.reads.map((d) => d.label).join(', ') || '—'}`);
  }
  if (S.feeds) {
    L.push(`\nFEEDS COMPONENTS (${S.feeds.length})`);
    L.push(list(S.feeds, (c) => `  ${c.label}`));
  }
  if (S.invalidatedBy) {
    L.push(`\nINVALIDATED BY (${S.invalidatedBy.length})`);
    L.push(list(S.invalidatedBy, (m) => `  ${m.label}  ${opt(m.optimistic)}`));
  }
  if (S.backs) {
    L.push(`\nBACKS QUERIES (${S.backs.length})`);
    L.push(list(S.backs, (q) => `  ${q.label}`));
  }
  if (S.writtenBy) {
    L.push(`\nWRITTEN BY (${S.writtenBy.length})`);
    L.push(list(S.writtenBy, (m) => `  ${m.label}`));
  }
  if (S.renders) {
    L.push(`\nRENDERS (${S.renders.length})`);
    L.push(list(S.renders, (c) => `  ${c.label}`));
  }
  if (S.tools) {
    L.push(`\nTOOLS (${S.tools.length})`);
    L.push(
      list(
        S.tools,
        (tool) =>
          `  ${tool.label}  mutation=${tool.mutation} minimum-integrity=${tool.minimumIntegrity} result-integrity=${tool.resultIntegrity}`,
      ),
    );
  }
  if (S.modelOperations) {
    L.push(
      `\nMODEL EFFECTS: ${S.modelOperations.map((operation) => operation.kind).join(', ') || '—'}`,
    );
  }
  if (S.invokes) {
    L.push(`\nINVOKES MUTATIONS (${S.invokes.length})`);
    L.push(list(S.invokes, (mutation) => `  ${mutation.label}`));
  }
  if (S.operations) {
    L.push(`\nTOOL EFFECTS: ${S.operations.map((operation) => operation.kind).join(', ') || '—'}`);
  }
  if (S.composition) {
    if (card.cron) L.push(`cron: ${card.cron}`);
    L.push(`\nTASK EDGES (${S.composition.length})`);
    L.push(list(S.composition, (edge) => `  ${edge.edge} → ${edge.label}`));
  }
  if (card.source) {
    L.push(`\nSOURCE  ${card.source.file}:${card.source.startLine}-${card.source.endLine}`);
    L.push(
      card.source.code
        .split('\n')
        .map((l) => `  ${l}`)
        .join('\n'),
    );
  }
  return L.join('\n');
}
```
```js title="packages/devtool/src/render.mjs"
// Server-side renderer for the dataflow graph. URL-driven (SPEC §8): ?app, ?sel,
// ?q decide what renders, so the core works JS-off. Pure — takes a prebuilt bundle
// (nodes/edges with source slices) and returns HTML.
import { buildBm25, KIND_META, LANES, laneForKind, traceGraph } from './graph-model.mjs';
import { renderCode } from './highlight.mjs';
import {
  arrayAppend,
  arrayFilter,
  arrayFind,
  arrayIncludes,
  arrayLength,
  arrayMap,
  arrayReduce,
  arrayReverseCopy,
  arraySlice,
  arraySome,
  arraySort,
  arrayValue,
  createMap,
  createSet,
  defineOwnData,
  encodeQueryValue,
  escapeHtmlAttribute,
  escapeHtmlText,
  joinStrings,
  mapGet,
  mapSet,
  numberToFixed,
  setAdd,
  setHas,
  stringSplit,
  stringStartsWith,
  stringTrim,
} from './output-security.mjs';
import { snapshotRenderOptions } from './render-input.mjs';

const W = 176;
const H = 56;
const COL_GAP = 78;
const COL_STEP = W + COL_GAP;
const ROW_STEP = 86;
const X0 = 40;
const TOP_PAD = 80;

const escAttr = escapeHtmlAttribute;
const esc = escapeHtmlText;

function queryHref(params) {
  const pairs = [];
  const keys = ['app', 'sel', 'q'];
  for (let index = 0; index < arrayLength(keys, 'devtool query keys'); index += 1) {
    const key = arrayValue(keys, index, 'devtool query keys');
    const value = params[key];
    if (value !== undefined && value !== null && value !== '') {
      if (typeof value !== 'string')
        throw new TypeError(`Devtool query parameter ${key} must be text.`);
      arrayAppend(pairs, `${key}=${encodeQueryValue(value)}`, 'devtool query parameters');
    }
  }
  return `?${joinStrings(pairs, '&', 'devtool query parameters')}`;
}

const accent = (kind) => KIND_META[kind]?.accent ?? '#888';
const glyph = (kind) => KIND_META[kind]?.glyph ?? '•';

// ---------- layout ----------
function layout(bundle) {
  const activeLanes = arrayFilter(
    LANES,
    (k) => arraySome(bundle.nodes, (n) => laneForKind(n.kind) === k, 'devtool graph nodes'),
    'devtool lane vocabulary',
  );
  const lanes = arrayMap(
    activeLanes,
    (k) => arrayFilter(bundle.nodes, (n) => laneForKind(n.kind) === k, 'devtool graph nodes'),
    'devtool active lanes',
  );

  const adj = createMap();
  for (let index = 0; index < arrayLength(bundle.nodes, 'devtool graph nodes'); index += 1) {
    const node = arrayValue(bundle.nodes, index, 'devtool graph nodes');
    mapSet(adj, node.id, []);
  }
  for (let index = 0; index < arrayLength(bundle.edges, 'devtool graph edges'); index += 1) {
    const edge = arrayValue(bundle.edges, index, 'devtool graph edges');
    arrayAppend(mapGet(adj, edge.from), edge.to, 'devtool adjacency');
    arrayAppend(mapGet(adj, edge.to), edge.from, 'devtool adjacency');
  }

  const rankOf = createMap();
  const reindex = () => {
    for (let laneIndex = 0; laneIndex < arrayLength(lanes, 'devtool lanes'); laneIndex += 1) {
      const lane = arrayValue(lanes, laneIndex, 'devtool lanes');
      for (let nodeIndex = 0; nodeIndex < arrayLength(lane, 'devtool lane'); nodeIndex += 1) {
        mapSet(rankOf, arrayValue(lane, nodeIndex, 'devtool lane').id, nodeIndex);
      }
    }
  };
  reindex();
  for (let sweep = 0; sweep < 6; sweep++) {
    const forwardOrder = [];
    for (let index = 0; index < arrayLength(lanes, 'devtool lanes'); index += 1) {
      arrayAppend(forwardOrder, index, 'devtool lane order');
    }
    const order = sweep % 2 ? arrayReverseCopy(forwardOrder, 'devtool lane order') : forwardOrder;
    for (
      let orderIndex = 0;
      orderIndex < arrayLength(order, 'devtool lane order');
      orderIndex += 1
    ) {
      const laneIndex = arrayValue(order, orderIndex, 'devtool lane order');
      const lane = arrayValue(lanes, laneIndex, 'devtool lanes');
      const bary = createMap();
      for (let nodeIndex = 0; nodeIndex < arrayLength(lane, 'devtool lane'); nodeIndex += 1) {
        const node = arrayValue(lane, nodeIndex, 'devtool lane');
        const neighbors = mapGet(adj, node.id);
        const ranks = [];
        for (
          let neighborIndex = 0;
          neighborIndex < arrayLength(neighbors, 'devtool adjacency');
          neighborIndex += 1
        ) {
          const rank = mapGet(rankOf, arrayValue(neighbors, neighborIndex, 'devtool adjacency'));
          if (rank !== undefined) arrayAppend(ranks, rank, 'devtool neighbor ranks');
        }
        mapSet(
          bary,
          node.id,
          arrayLength(ranks, 'devtool neighbor ranks') > 0
            ? arrayReduce(ranks, (sum, rank) => sum + rank, 0, 'devtool neighbor ranks') /
                arrayLength(ranks, 'devtool neighbor ranks')
            : nodeIndex,
        );
      }
      defineOwnData(
        lanes,
        laneIndex,
        arraySort(
          lane,
          (left, right) => mapGet(bary, left.id) - mapGet(bary, right.id) || 0,
          'devtool lane',
        ),
      );
      reindex();
    }
  }

  let maxRows = 1;
  for (let laneIndex = 0; laneIndex < arrayLength(lanes, 'devtool lanes'); laneIndex += 1) {
    const size = arrayLength(arrayValue(lanes, laneIndex, 'devtool lanes'), 'devtool lane');
    if (size > maxRows) maxRows = size;
  }
  for (let laneIndex = 0; laneIndex < arrayLength(lanes, 'devtool lanes'); laneIndex += 1) {
    const lane = arrayValue(lanes, laneIndex, 'devtool lanes');
    const startY = TOP_PAD + ((maxRows - arrayLength(lane, 'devtool lane')) * ROW_STEP) / 2;
    for (let nodeIndex = 0; nodeIndex < arrayLength(lane, 'devtool lane'); nodeIndex += 1) {
      const node = arrayValue(lane, nodeIndex, 'devtool lane');
      node.lane = laneIndex;
      node.x = X0 + laneIndex * COL_STEP;
      node.y = startY + nodeIndex * ROW_STEP;
    }
  }

  const width = X0 + (arrayLength(activeLanes, 'devtool active lanes') - 1) * COL_STEP + W + 44;
  const calculatedHeight = TOP_PAD + maxRows * ROW_STEP + 24;
  const height = calculatedHeight > 480 ? calculatedHeight : 480;
  return { activeLanes, width, height };
}

// ---------- trace ----------
function trace(bundle, selId) {
  return traceGraph(bundle.nodes, bundle.edges, selId);
}

function edgePath(a, b) {
  const forward = (a.lane ?? 0) <= (b.lane ?? 0);
  if (forward) {
    const sx = a.x + W,
      sy = a.y + H / 2,
      ex = b.x,
      ey = b.y + H / 2,
      c = COL_GAP * 0.5;
    return `M ${sx} ${sy} C ${sx + c} ${sy} ${ex - c} ${ey} ${ex} ${ey}`;
  }
  const sx = a.x + W / 2,
    sy = a.y + H,
    ex = b.x + W / 2,
    ey = b.y + H,
    cy = (sy > ey ? sy : ey) + 64;
  return `M ${sx} ${sy} C ${sx} ${cy} ${ex} ${cy} ${ex} ${ey}`;
}

// ---------- main ----------
export function renderPage(opts) {
  const { manifest, bundle, app, sel, q, pzHref, runtime } = snapshotRenderOptions(opts);
  const byId = createMap();
  for (let index = 0; index < arrayLength(bundle.nodes, 'devtool graph nodes'); index += 1) {
    const node = arrayValue(bundle.nodes, index, 'devtool graph nodes');
    mapSet(byId, node.id, node);
  }
  const { activeLanes, width, height } = layout(bundle);

  const selNode = sel ? mapGet(byId, sel) : undefined;
  const tr = selNode ? trace(bundle, selNode.id) : null;
  const live = runtimeState(runtime);

  let results = '';
  let hits = createSet();
  if (q && stringTrim(q)) {
    const search = buildBm25(bundle.nodes);
    const ranked = search(q, 6);
    hits = createSet();
    for (let index = 0; index < arrayLength(ranked, 'devtool search results'); index += 1) {
      setAdd(hits, arrayValue(ranked, index, 'devtool search results').id);
    }
    results =
      `<div class="results"><div class="results-head"><span>BM25 · ${ranked.length} matches</span><span>over ${bundle.nodes.length} graph cards</span></div>` +
      (ranked.length
        ? joinStrings(
            arrayMap(
              ranked,
              (r) => {
                const n = mapGet(byId, r.id);
                return (
                  `<a class="result" href="${escAttr(queryHref({ app, sel: n.id, q }))}">` +
                  `<span class="dot" style="background:${escAttr(accent(n.kind))}"></span>` +
                  `<span><b>${esc(n.label)}</b> <span class="chip">${esc(n.kind)}</span></span>` +
                  `<span class="matched">${esc(joinStrings(r.matched, ' ', 'devtool matched terms'))}</span><span class="score">${esc(numberToFixed(r.score, 2))}</span></a>`
                );
              },
              'devtool search results',
            ),
            '',
            'devtool rendered search results',
          )
        : `<div class="result"><span style="color:var(--faint)">No graph cards matched “${esc(q)}”.</span></div>`) +
      `</div>`;
  }

  const markers = joinStrings(
    arrayMap(
      activeLanes,
      (k) =>
        `<marker id="ar-${escAttr(k)}" markerWidth="7" markerHeight="7" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="${escAttr(accent(k))}"/></marker>`,
      'devtool active lanes',
    ),
    '',
    'devtool SVG markers',
  );
  const paths = joinStrings(
    arrayMap(
      bundle.edges,
      (e) => {
        const a = mapGet(byId, e.from),
          b = mapGet(byId, e.to);
        if (!a || !b) return '';
        const col = accent(a.kind);
        const active = tr ? setHas(tr.edges, e.id) : false;
        const runtimeActive = runtimeEdgeIsActive(live.nodes, e.from, e.to);
        const cls = joinStrings(
          arrayFilter(
            [
              'edge',
              tr ? (active ? 'active animated' : 'dim') : '',
              runtimeActive ? 'runtime-hot' : '',
              live.pending && runtimeActive ? 'runtime-pending' : '',
            ],
            (value) => value !== '',
          ),
          ' ',
        );
        return `<path class="${escAttr(cls)}" data-from="${escAttr(e.from)}" data-to="${escAttr(e.to)}" d="${escAttr(edgePath(a, b))}" stroke="${escAttr(col)}" stroke-opacity="0.32" style="color:${escAttr(col)}" marker-end="url(#ar-${escAttr(a.kind)})"/>`;
      },
      'devtool graph edges',
    ),
    '',
    'devtool SVG paths',
  );

  const cards = joinStrings(
    arrayMap(
      bundle.nodes,
      (n) => {
        const cls = ['node', `node--${n.kind}`];
        if (selNode) {
          if (n.id === selNode.id) arrayAppend(cls, 'sel', 'devtool node classes');
          else if (tr && setHas(tr.nodes, n.id)) arrayAppend(cls, 'trace', 'devtool node classes');
          else arrayAppend(cls, 'dim', 'devtool node classes');
        }
        if (setHas(hits, n.id)) arrayAppend(cls, 'hit', 'devtool node classes');
        if (setHas(live.nodes, n.id)) {
          arrayAppend(cls, 'runtime-hot', 'devtool node classes');
          if (live.pending) arrayAppend(cls, 'runtime-pending', 'devtool node classes');
        }
        const sub = nodeSub(n);
        const href = queryHref({ app, sel: n.id, q });
        return (
          `<a class="${escAttr(joinStrings(cls, ' ', 'devtool node classes'))}" data-node-id="${escAttr(n.id)}" href="${escAttr(href)}" style="left:${escAttr(n.x)}px;top:${escAttr(n.y)}px;width:${W}px;min-height:${H}px">` +
          `<span class="label"><span class="glyph">${esc(glyph(n.kind))}</span>${esc(n.label)}</span>` +
          (sub ? `<span class="sub">${esc(sub)}</span>` : '') +
          `</a>`
        );
      },
      'devtool graph nodes',
    ),
    '',
    'devtool node cards',
  );

  const laneHeads = joinStrings(
    arrayMap(
      activeLanes,
      (k, i) => {
        const x = X0 + i * COL_STEP + W / 2;
        const m = KIND_META[k];
        return `<div class="lane-head" style="left:${escAttr(x)}px;color:${escAttr(m.accent)}"><span class="glyph">${esc(m.glyph)}</span><span class="name">${esc(m.label)}</span><span class="blurb">${esc(m.blurb)}</span></div>`;
      },
      'devtool active lanes',
    ),
    '',
    'devtool lane headings',
  );

  const legend = joinStrings(
    arrayMap(
      activeLanes,
      (k) =>
        `<span class="k"><span class="sw" style="background:${escAttr(accent(k))}"></span>${esc(KIND_META[k].label)}</span>`,
      'devtool active lanes',
    ),
    '',
    'devtool legend',
  );
  const appTabs = joinStrings(
    arrayMap(
      manifest,
      (a) =>
        `<a class="app-tab" href="${escAttr(queryHref({ app: a.id }))}" aria-current="${escAttr(a.id === app ? 'true' : 'false')}"><b>${esc(a.label)}</b><small>${esc(a.blurb)}</small></a>`,
      'devtool manifest',
    ),
    '',
    'devtool app tabs',
  );

  return (
    `<div class="app">` +
    `<header class="topbar">` +
    `<div class="brand"><span class="brand-mark"></span><span class="brand-name">Kovo</span><span class="brand-sub">Dataflow</span></div>` +
    `<form class="search" method="get" action="" role="search">` +
    `<input type="hidden" name="app" value="${escAttr(app)}"/>` +
    `<svg viewBox="0 0 24 24"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>` +
    `<input name="q" value="${escAttr(q ?? '')}" placeholder="Trace anything — a component, query, mutation…" autocomplete="off" spellcheck="false"/>` +
    `<kbd>BM25</kbd>${results}</form>` +
    `<div class="spacer"></div><div class="apps">${appTabs}</div></header>` +
    `<div class="stage"><div class="canvas-wrap">` +
    `<div class="canvas" data-pz-root kovo-c="dataflow-canvas" kovo-state="{}" on:visible="${escAttr(pzHref)}#Devtool$init" style="width:${escAttr(width)}px;height:${escAttr(height)}px">` +
    `<div class="pz" data-pz><div class="lane-headers">${laneHeads}</div>` +
    `<svg class="edges" width="${width}" height="${height}"><defs>${markers}</defs>${paths}</svg>${cards}</div></div>` +
    (selNode
      ? ''
      : `<div class="hint">Select a <b>component</b> to trace its <b>queries in</b> and <b>mutations out</b> · scroll to zoom · drag to pan</div>`) +
    `<div class="legend">${legend}</div>` +
    (runtime === undefined ? '' : renderRuntimePanel(app, runtime)) +
    `<div class="zoom"><button type="button" data-zoom="out" title="Zoom out (−)">−</button><button type="button" data-zoom="fit" title="Fit (0)">⤢</button><button type="button" data-zoom="in" title="Zoom in (+)">+</button></div>` +
    `</div><aside class="inspector">${renderInspector(bundle, byId, selNode)}</aside></div></div>`
  );
}

function runtimeState(runtime) {
  const nodes = createSet();
  if (runtime === undefined || arrayLength(runtime.frames, 'runtime frames') === 0) {
    return { nodes, pending: false };
  }
  const frame = arrayValue(
    runtime.frames,
    arrayLength(runtime.frames, 'runtime frames') - 1,
    'runtime frames',
  );
  if (frame.mutation) setAdd(nodes, `mutation:${frame.mutation}`);
  for (let index = 0; index < arrayLength(frame.changes, 'runtime changes'); index += 1) {
    setAdd(nodes, `domain:${arrayValue(frame.changes, index, 'runtime changes').domain}`);
  }
  for (
    let index = 0;
    index < arrayLength(frame.targets.queryNames, 'runtime target queries');
    index += 1
  ) {
    setAdd(nodes, `query:${arrayValue(frame.targets.queryNames, index, 'runtime target queries')}`);
  }
  for (let index = 0; index < arrayLength(frame.queries, 'runtime queries'); index += 1) {
    setAdd(nodes, `query:${arrayValue(frame.queries, index, 'runtime queries').name}`);
  }
  return { nodes, pending: frame.phase === 'pending' };
}

function runtimeEdgeIsActive(nodes, from, to) {
  if (!setHas(nodes, from)) return false;
  return (
    setHas(nodes, to) || stringStartsWith(from, 'mutation:') || stringStartsWith(from, 'query:')
  );
}

function renderRuntimePanel(app, runtime) {
  const streamHref = `${runtime.href}?app=${encodeQueryValue(app)}`;
  const rows = joinStrings(
    arrayMap(
      arrayReverseCopy(runtime.frames, 'runtime frames'),
      (frame) =>
        `<li class="runtime-row runtime-row--${escAttr(frame.phase)}" data-runtime-sequence="${escAttr(frame.sequence)}">` +
        `<span class="runtime-phase">${esc(frame.phase)}</span>` +
        `<span class="runtime-summary">${esc(runtimeFrameSummary(frame))}</span></li>`,
      'runtime frames',
    ),
    '',
    'runtime frame rows',
  );
  return (
    `<section class="runtime-panel" aria-label="Recent runtime frames" data-runtime-panel data-runtime-stream="${escAttr(streamHref)}" ` +
    `kovo-c="dataflow-runtime" kovo-state="{}" on:visible="${escAttr(runtime.moduleHref)}#DevtoolRuntime$init">` +
    `<header><span><i aria-hidden="true"></i> Runtime replay</span><small role="status" aria-live="polite" data-runtime-status>snapshot</small></header>` +
    `<ol aria-live="polite" aria-relevant="additions text" data-runtime-list>${rows || '<li class="runtime-empty">No enhanced round-trips yet.</li>'}</ol>` +
    `<footer>Values, keys, target identities, inputs, cookies, and bodies stay redacted.</footer>` +
    `</section>`
  );
}

function runtimeFrameSummary(frame) {
  const parts = [`#${frame.sequence}`];
  if (frame.mutation) arrayAppend(parts, frame.mutation, 'runtime frame summary');
  if (arrayLength(frame.changes, 'runtime changes') > 0) {
    arrayAppend(
      parts,
      `changes ${joinStrings(
        arrayMap(frame.changes, (change) => change.domain, 'runtime changes'),
        ', ',
        'runtime change domains',
      )}`,
      'runtime frame summary',
    );
  }
  const queryNames = createSet();
  const names = [];
  for (
    let index = 0;
    index < arrayLength(frame.targets.queryNames, 'runtime target queries');
    index += 1
  ) {
    const name = arrayValue(frame.targets.queryNames, index, 'runtime target queries');
    if (!setHas(queryNames, name)) {
      setAdd(queryNames, name);
      arrayAppend(names, name, 'runtime query names');
    }
  }
  let queryBytes = 0;
  for (let index = 0; index < arrayLength(frame.queries, 'runtime queries'); index += 1) {
    const query = arrayValue(frame.queries, index, 'runtime queries');
    if (!setHas(queryNames, query.name)) {
      setAdd(queryNames, query.name);
      arrayAppend(names, query.name, 'runtime query names');
    }
    queryBytes += query.bytes;
  }
  if (arrayLength(names, 'runtime query names') > 0) {
    arrayAppend(
      parts,
      `queries ${joinStrings(names, ', ', 'runtime query names')}`,
      'runtime frame summary',
    );
  }
  if (arrayLength(frame.queries, 'runtime queries') > 0) {
    arrayAppend(parts, `${queryBytes} B values redacted`, 'runtime frame summary');
  }
  if (frame.status !== undefined) {
    arrayAppend(parts, `HTTP ${frame.status}`, 'runtime frame summary');
  }
  if (frame.truncated || frame.targets.truncated) {
    arrayAppend(parts, 'truncated', 'runtime frame summary');
  }
  return joinStrings(parts, ' · ', 'runtime frame summary');
}

function nodeSub(n) {
  if (n.kind === 'diagnostic') return `${n.data.severity} · ${n.data.category}`;
  if (n.kind === 'agent')
    return `${arrayLength(n.data.modelOperations, 'devtool agent model operations')} model effects`;
  if (n.kind === 'tool')
    return `${n.data.minimumIntegrity || 'unknown'} → ${n.data.mutation || 'unresolved'}`;
  if (n.kind === 'task') return n.data.cron ? `cron ${n.data.cron}` : 'durable work';
  if (n.kind === 'mutation') return joinStrings(n.data.writes, ' · ', 'devtool writes');
  if (n.kind === 'query')
    return 'reads ' + joinStrings(n.data.domains, ', ', 'devtool query domains');
  if (n.kind === 'component') return n.data.domName ?? '';
  if (n.kind === 'page') return 'route';
  return '';
}

function fileLeaf(path) {
  const parts = stringSplit(path, '/');
  const length = arrayLength(parts, 'devtool source path');
  return length === 0 ? '' : arrayValue(parts, length - 1, 'devtool source path');
}

// ---------- inspector ----------
const statusBadge = (s) => {
  if (!s) return `<span class="badge badge--none">no transform</span>`;
  const st = s.status;
  if (st === 'derived') return `<span class="badge badge--derived">derived</span>`;
  if (st === 'hand-written') return `<span class="badge badge--hand-written">hand-written</span>`;
  if (s.derivation?.status === 'PUNTED')
    return `<span class="badge badge--punted" title="${escAttr(puntReasonTitle(s.derivation.reason))}">punted · ${esc(s.derivation.reason?.code ?? '')}</span>`;
  if (st === 'await-fragment')
    return `<span class="badge badge--await-fragment">await-fragment</span>`;
  return `<span class="badge badge--none">${esc(st)}</span>`;
};

function puntReasonTitle(reason) {
  if (!reason) return 'PUNTED';
  const parts = [`PUNTED code=${reason.code}`];
  const fields = ['site', 'field', 'expr', 'column', 'shape', 'table', 'detail'];
  for (let index = 0; index < arrayLength(fields, 'devtool punt-reason fields'); index += 1) {
    const field = arrayValue(fields, index, 'devtool punt-reason fields');
    if (reason[field]) arrayAppend(parts, `${field}=${reason[field]}`, 'devtool punt reason');
  }
  const columns = reason.columns ?? [];
  if (arrayLength(columns, 'devtool punt-reason columns')) {
    arrayAppend(
      parts,
      `columns=${joinStrings(columns, ',', 'devtool punt-reason columns')}`,
      'devtool punt reason',
    );
  }
  return joinStrings(parts, '; ', 'devtool punt reason');
}

function flowrow(app, n, right, q) {
  const href = queryHref({ app, sel: n.id, q });
  return (
    `<a class="flowrow node--${escAttr(n.kind)}" href="${escAttr(href)}"><span class="dot"></span><span class="name">${esc(n.label)}` +
    (n.kind === 'component' && n.data.domName ? ` <small>${esc(n.data.domName)}</small>` : '') +
    `</span><span class="right">${right}</span></a>`
  );
}

function renderInspector(bundle, byId, sel) {
  const app = bundle.app;
  if (!sel) return overviewInspector(bundle);

  const out = arrayFilter(bundle.edges, (e) => e.from === sel.id, 'devtool graph edges');
  const inc = arrayFilter(bundle.edges, (e) => e.to === sel.id, 'devtool graph edges');
  const mutsWriting = (domains) =>
    arrayFilter(
      bundle.nodes,
      (n) =>
        n.kind === 'mutation' &&
        arraySome(
          n.data.writes,
          (domain) => arrayIncludes(domains, domain, 'devtool target domains'),
          'devtool mutation writes',
        ),
      'devtool graph nodes',
    );
  const optStatus = (m, queryName) =>
    arrayFind(m.data.optimistic, (o) => o.query === queryName, 'devtool optimistic facts') ?? null;

  let body = '';
  if (sel.kind === 'diagnostic') {
    const source = sel.data.sourceAnchor;
    body += section(
      'Message',
      0,
      `<div style="font-size:13px;line-height:1.6;color:var(--ink)">${esc(sel.data.message)}</div>`,
    );
    if (sel.data.help) {
      body += section(
        'Next step',
        0,
        `<div style="font-size:13px;line-height:1.6;color:var(--dim)">${esc(sel.data.help)}</div>`,
      );
    }
    body += section(
      'Producer facts',
      0,
      `<div class="kv"><span class="chip">${esc(sel.data.code)}</span><span class="chip">${esc(sel.data.severity)}</span><span class="chip">${esc(sel.data.category)}</span>${
        source
          ? `<span class="chip">${esc(source.file)}:${esc(source.start)}-${esc(source.end)}</span>`
          : ''
      }</div>`,
    );
  } else if (sel.kind === 'agent' || sel.kind === 'tool' || sel.kind === 'task') {
    const outgoing = arrayMap(
      out,
      (edge) => ({ edge, node: mapGet(byId, edge.to) }),
      'devtool authored graph outgoing edges',
    );
    const incoming = arrayMap(
      inc,
      (edge) => ({ edge, node: mapGet(byId, edge.from) }),
      'devtool authored graph incoming edges',
    );
    body += section(
      'Outgoing graph facts',
      arrayLength(outgoing, 'devtool authored graph outgoing edges'),
      joinStrings(
        arrayMap(
          outgoing,
          (fact) => flowrow(app, fact.node, `<span class="chip">${esc(fact.edge.kind)}</span>`),
          'devtool authored graph outgoing edges',
        ),
        '',
        'devtool authored graph outgoing rows',
      ) || muted('No outgoing graph facts.'),
    );
    if (arrayLength(incoming, 'devtool authored graph incoming edges') > 0) {
      body += section(
        'Incoming graph facts',
        arrayLength(incoming, 'devtool authored graph incoming edges'),
        joinStrings(
          arrayMap(
            incoming,
            (fact) => flowrow(app, fact.node, `<span class="chip">${esc(fact.edge.kind)}</span>`),
            'devtool authored graph incoming edges',
          ),
          '',
          'devtool authored graph incoming rows',
        ),
      );
    }
    const operations =
      sel.kind === 'agent'
        ? sel.data.modelOperations
        : sel.kind === 'tool'
          ? sel.data.operations
          : [];
    if (arrayLength(operations, 'devtool security operations') > 0) {
      body += section(
        sel.kind === 'agent' ? 'Model effects' : 'Tool effects',
        arrayLength(operations, 'devtool security operations'),
        `<div class="kv">${joinStrings(
          arrayMap(
            operations,
            (operation) => `<span class="chip">${esc(operation.kind)}</span>`,
            'devtool security operations',
          ),
          '',
          'devtool operation chips',
        )}</div>`,
      );
    }
    if (sel.kind === 'tool') {
      body += section(
        'Integrity contract',
        0,
        `<div class="kv"><span class="chip">minimum ${esc(sel.data.minimumIntegrity || 'unknown')}</span><span class="chip">result ${esc(sel.data.resultIntegrity || 'unknown')}</span></div>`,
      );
    }
    if (sel.kind === 'task' && sel.data.cron) {
      body += section(
        'Schedule',
        0,
        `<div class="kv"><span class="chip">${esc(sel.data.cron)}</span></div>`,
      );
    }
  } else if (sel.kind === 'component') {
    const queries = arrayMap(
      arrayFilter(inc, (e) => e.kind === 'feeds', 'devtool incoming edges'),
      (e) => mapGet(byId, e.from),
      'devtool query edges',
    );
    const mutations = arrayMap(
      arrayFilter(out, (e) => e.kind === 'emits', 'devtool outgoing edges'),
      (e) => mapGet(byId, e.to),
      'devtool mutation edges',
    );
    body += section(
      'Queries in',
      arrayLength(queries, 'devtool component queries'),
      joinStrings(
        arrayMap(
          queries,
          (qn) =>
            flowrow(
              app,
              qn,
              joinStrings(
                arrayMap(
                  qn.data.domains,
                  (d) => `<span class="chip chip--domain">${esc(d)}</span>`,
                  'devtool query domains',
                ),
                '',
                'devtool domain chips',
              ),
            ),
          'devtool component queries',
        ),
        '',
        'devtool component query rows',
      ) || muted('No query dependencies.'),
    );
    body += section(
      'Mutations out',
      arrayLength(mutations, 'devtool component mutations'),
      arrayLength(mutations, 'devtool component mutations')
        ? joinStrings(
            arrayMap(
              mutations,
              (m) =>
                flowrow(
                  app,
                  m,
                  joinStrings(
                    arrayMap(
                      arraySlice(m.data.inputFields, 0, 4, 'devtool mutation input fields'),
                      (f) => `<span class="chip">${esc(f)}</span>`,
                      'devtool visible input fields',
                    ),
                    '',
                    'devtool input chips',
                  ),
                ),
              'devtool component mutations',
            ),
            '',
            'devtool component mutation rows',
          )
        : muted('No mutations emitted (read-only component).'),
    );
    const behavior = arrayMap(
      arrayFilter(
        out,
        (e) => e.kind === 'handles' || e.kind === 'triggers' || e.kind === 'owns',
        'devtool component behavior edges',
      ),
      (e) => mapGet(byId, e.to),
      'devtool component behavior nodes',
    );
    const deriveBehavior = arrayMap(
      arrayFilter(inc, (e) => e.kind === 'derives', 'devtool component derive edges'),
      (e) => mapGet(byId, e.from),
      'devtool component derive nodes',
    );
    for (
      let behaviorIndex = 0;
      behaviorIndex < arrayLength(deriveBehavior, 'devtool component derive nodes');
      behaviorIndex += 1
    ) {
      arrayAppend(
        behavior,
        arrayValue(deriveBehavior, behaviorIndex, 'devtool component derive nodes'),
        'devtool component behavior nodes',
      );
    }
    body += section(
      'Authored behavior',
      arrayLength(behavior, 'devtool component behavior'),
      joinStrings(
        arrayMap(
          behavior,
          (detail) =>
            flowrow(app, detail, `<span class="chip">${esc(KIND_META[detail.kind].label)}</span>`),
          'devtool component behavior',
        ),
        '',
        'devtool component behavior rows',
      ) || muted('No anchored handlers, triggers, derives, or binding positions.'),
    );
    const cov = [];
    for (
      let queryIndex = 0;
      queryIndex < arrayLength(queries, 'devtool component queries');
      queryIndex += 1
    ) {
      const qn = arrayValue(queries, queryIndex, 'devtool component queries');
      const writers = mutsWriting(qn.data.domains);
      for (
        let writerIndex = 0;
        writerIndex < arrayLength(writers, 'devtool query writers');
        writerIndex += 1
      ) {
        const mutation = arrayValue(writers, writerIndex, 'devtool query writers');
        arrayAppend(
          cov,
          `<a class="flowrow node--mutation" href="${escAttr(queryHref({ app, sel: mutation.id }))}"><span class="dot"></span><span class="name">${esc(mutation.label)} <small>→ ${esc(qn.label)}</small></span><span class="right">${statusBadge(optStatus(mutation, qn.name))}</span></a>`,
          'devtool refresh coverage',
        );
      }
    }
    body += section(
      'Refresh coverage',
      arrayLength(cov, 'devtool refresh coverage'),
      joinStrings(cov, '', 'devtool refresh coverage') ||
        muted('Nothing invalidates this component’s data.'),
    );
  } else if (sel.kind === 'mutation') {
    const domains = sel.data.writes;
    const queries = arrayFilter(
      bundle.nodes,
      (n) =>
        n.kind === 'query' &&
        arraySome(
          n.data.domains,
          (domain) => arrayIncludes(domains, domain, 'devtool mutation domains'),
          'devtool query domains',
        ),
      'devtool graph nodes',
    );
    body += section(
      'Writes domains',
      arrayLength(domains, 'devtool mutation domains'),
      `<div class="kv">${joinStrings(
        arrayMap(
          domains,
          (d) => chipLink(app, mapGet(byId, `domain:${d}`)),
          'devtool mutation domains',
        ),
        '',
        'devtool domain links',
      )}</div>`,
    );
    body += section(
      'Invalidates queries',
      arrayLength(queries, 'devtool invalidated queries'),
      joinStrings(
        arrayMap(
          queries,
          (qn) => flowrow(app, qn, statusBadge(optStatus(sel, qn.name))),
          'devtool invalidated queries',
        ),
        '',
        'devtool invalidated query rows',
      ) || muted('No queries read these domains.'),
    );
    if (arrayLength(sel.data.inputFields, 'devtool mutation input fields'))
      body += section(
        'Input fields',
        arrayLength(sel.data.inputFields, 'devtool mutation input fields'),
        `<div class="kv">${joinStrings(
          arrayMap(
            sel.data.inputFields,
            (f) => `<span class="chip">${esc(f)}</span>`,
            'devtool mutation input fields',
          ),
          '',
          'devtool input field chips',
        )}</div>`,
      );
    const touches = sel.source?.touches ?? [];
    if (arrayLength(touches, 'devtool touch sites'))
      body += section(
        'Write sites (touch graph)',
        arrayLength(touches, 'devtool touch sites'),
        joinStrings(
          arrayMap(
            touches,
            (t) =>
              `<div class="touch"><span class="via">${esc(t.via)}</span><span class="chip chip--domain">${esc(t.domain)}</span>${t.keys ? `<span class="chip">${esc(t.keys)}</span>` : ''}<span class="site">${esc(fileLeaf(t.site))}</span></div>`,
            'devtool touch sites',
          ),
          '',
          'devtool touch rows',
        ),
      );
  } else if (sel.kind === 'query') {
    const domains = sel.data.domains;
    const consumers = arrayMap(
      arrayFilter(
        bundle.edges,
        (e) => e.kind === 'feeds' && e.from === sel.id,
        'devtool graph edges',
      ),
      (e) => mapGet(byId, e.to),
      'devtool consumer edges',
    );
    const invalidators = mutsWriting(domains);
    body += section(
      'Reads domains',
      arrayLength(domains, 'devtool query domains'),
      `<div class="kv">${joinStrings(
        arrayMap(
          domains,
          (d) => chipLink(app, mapGet(byId, `domain:${d}`)),
          'devtool query domains',
        ),
        '',
        'devtool domain links',
      )}</div>`,
    );
    body += section(
      'Feeds components',
      arrayLength(consumers, 'devtool query consumers'),
      joinStrings(
        arrayMap(consumers, (c) => flowrow(app, c, ''), 'devtool query consumers'),
        '',
        'devtool consumer rows',
      ) || muted('No component consumes this query yet.'),
    );
    body += section(
      'Invalidated by',
      arrayLength(invalidators, 'devtool query invalidators'),
      joinStrings(
        arrayMap(
          invalidators,
          (m) => flowrow(app, m, statusBadge(optStatus(m, sel.name))),
          'devtool query invalidators',
        ),
        '',
        'devtool invalidator rows',
      ) || muted('No mutation invalidates this query.'),
    );
  } else if (sel.kind === 'domain') {
    const queries = arrayFilter(
      bundle.nodes,
      (n) => n.kind === 'query' && arrayIncludes(n.data.domains, sel.name, 'devtool query domains'),
      'devtool graph nodes',
    );
    const writers = arrayFilter(
      bundle.nodes,
      (n) =>
        n.kind === 'mutation' && arrayIncludes(n.data.writes, sel.name, 'devtool mutation writes'),
      'devtool graph nodes',
    );
    body += section(
      'Backs queries',
      arrayLength(queries, 'devtool domain queries'),
      joinStrings(
        arrayMap(queries, (qn) => flowrow(app, qn, ''), 'devtool domain queries'),
        '',
        'devtool domain query rows',
      ) || muted('No query reads this domain.'),
    );
    body += section(
      'Written by',
      arrayLength(writers, 'devtool domain writers'),
      joinStrings(
        arrayMap(writers, (m) => flowrow(app, m, ''), 'devtool domain writers'),
        '',
        'devtool domain writer rows',
      ) || muted('No mutation writes this domain.'),
    );
  } else if (sel.kind === 'page') {
    const comps = arrayMap(
      arrayFilter(out, (e) => e.kind === 'renders', 'devtool outgoing edges'),
      (e) => mapGet(byId, e.to),
      'devtool page component edges',
    );
    if (sel.data.meta?.description)
      body += section(
        'Meta',
        0,
        `<div style="font-size:13px;color:var(--dim)">${esc(sel.data.meta.description)}</div>`,
      );
    body += section(
      'Renders',
      arrayLength(comps, 'devtool page components'),
      joinStrings(
        arrayMap(comps, (c) => flowrow(app, c, ''), 'devtool page components'),
        '',
        'devtool page component rows',
      ) || muted('No tracked component leaves on this route.'),
    );
  }

  if (sel.source?.code) body += section('Source', 0, renderCode(sel.source));

  const meta =
    sel.kind === 'diagnostic'
      ? sel.data.sourceAnchor
        ? `${sel.data.severity} · ${sel.data.sourceAnchor.file}:${sel.data.sourceAnchor.start}-${sel.data.sourceAnchor.end}`
        : `${sel.data.severity} · source-less`
      : sel.kind === 'agent'
        ? 'capability-bounded model'
        : sel.kind === 'tool'
          ? `invokes ${sel.data.mutation || 'unresolved mutation'}`
          : sel.kind === 'task'
            ? sel.data.cron
              ? `cron ${sel.data.cron}`
              : 'durable task'
            : sel.kind === 'component'
              ? `${sel.data.domName} · fragment ${arrayLength(sel.data.fragments, 'devtool fragments') ? arrayValue(sel.data.fragments, 0, 'devtool fragments') : '—'}`
              : sel.kind === 'mutation'
                ? `POST /_m/${sel.name}`
                : sel.kind === 'query'
                  ? `GET /_q/${sel.name}`
                  : sel.kind;
  const guards = sel.data.guards;
  return (
    `<div class="insp-head node--${escAttr(sel.kind)}"><span class="insp-kind" style="color:${escAttr(accent(sel.kind))}">${esc(glyph(sel.kind))} ${esc(sel.kind)}</span>` +
    `<div class="insp-title">${esc(sel.label)}</div><div class="insp-meta">${esc(meta)}</div>` +
    (arrayLength(guards, 'devtool guards')
      ? `<div class="kv" style="margin-top:8px">${joinStrings(
          arrayMap(
            guards,
            (guard) => `<span class="chip">🛡 ${esc(guard)}</span>`,
            'devtool guards',
          ),
          '',
          'devtool guard chips',
        )}</div>`
      : '') +
    `</div><div class="insp-body">${body}</div>`
  );
}

function overviewInspector(bundle) {
  const order = LANES;
  const counts = joinStrings(
    arrayMap(
      arrayFilter(order, (k) => bundle.counts[k] > 0, 'devtool overview order'),
      (k) =>
        `<a class="flowrow node--${escAttr(k)}" href="#"><span class="dot"></span><span class="name">${esc(KIND_META[k].label)}</span><span class="right"><span class="chip">${bundle.counts[k]}</span></span></a>`,
      'devtool visible overview kinds',
    ),
    '',
    'devtool overview rows',
  );
  const opt = createMap();
  for (
    let nodeIndex = 0;
    nodeIndex < arrayLength(bundle.nodes, 'devtool graph nodes');
    nodeIndex += 1
  ) {
    const node = arrayValue(bundle.nodes, nodeIndex, 'devtool graph nodes');
    for (
      let optimisticIndex = 0;
      optimisticIndex < arrayLength(node.data.optimistic, 'devtool optimistic facts');
      optimisticIndex += 1
    ) {
      const optimistic = arrayValue(
        node.data.optimistic,
        optimisticIndex,
        'devtool optimistic facts',
      );
      const key = optimistic.derivation?.status === 'PUNTED' ? 'punted' : optimistic.status;
      mapSet(opt, key, (mapGet(opt, key) ?? 0) + 1);
    }
  }
  const coverageKinds = ['derived', 'hand-written', 'await-fragment', 'UNHANDLED', 'punted'];
  const cov = joinStrings(
    arrayMap(
      arrayFilter(coverageKinds, (kind) => mapGet(opt, kind) !== undefined),
      (kind) =>
        `${statusBadge(kind === 'punted' ? { derivation: { status: 'PUNTED', reason: { code: '' } } } : { status: kind })} <span class="chip">${mapGet(opt, kind)}</span>`,
      'devtool optimistic coverage kinds',
    ),
    ' ',
    'devtool optimistic coverage badges',
  );
  const limitations = joinStrings(
    arrayMap(
      bundle.limitations,
      (limitation) =>
        `<div style="font-size:13px;line-height:1.5;color:var(--dim);margin-bottom:8px">${esc(limitation)}</div>`,
      'devtool graph limitations',
    ),
    '',
    'devtool graph limitation rows',
  );
  const parity =
    bundle.view === 'runtime-registry'
      ? 'This bounded runtime-registry view is for live navigation. Use <code style="font-family:var(--mono);color:var(--teal)">kovo explain</code> for the authoritative source and proof graph.'
      : 'This is the same graph the MCP <code style="font-family:var(--mono);color:var(--teal)">kovo_explain</code> tool returns to an agent.';
  return (
    `<div class="insp-head"><span class="insp-kind" style="color:var(--dim)">◫ overview</span>` +
    `<div class="insp-title">${esc(bundle.label)}</div><div class="insp-meta">${bundle.nodes.length} nodes · ${bundle.edges.length} edges · ${esc(bundle.provenance)}</div></div>` +
    `<div class="insp-body">` +
    section('Graph', 0, counts) +
    (cov
      ? section(
          'Optimistic coverage (SPEC §10.6)',
          0,
          `<div class="kv" style="align-items:center">${cov}</div>`,
        )
      : '') +
    (limitations ? section('Coverage limitations', 0, limitations) : '') +
    section(
      'How to read this',
      0,
      `<div style="font-size:13px;line-height:1.6;color:var(--dim)">Data flows left → right from capability-bounded agents, tools, and durable tasks through mutations and domains into queries, components, and pages. Producer-owned diagnostics retain their exact severity, help, and source span in a final presentation lane. Click any node to trace it. ${parity}</div>`,
    ) +
    `</div>`
  );
}

const section = (title, count, inner) =>
  `<div class="section"><h3>${esc(title)}${count ? `<span class="count">${count}</span>` : ''}</h3>${inner}</div>`;
const muted = (t) => `<div style="font-size:13px;color:var(--faint)">${esc(t)}</div>`;
const chipLink = (app, n) =>
  n
    ? `<a class="chip chip--domain" style="text-decoration:none" href="${escAttr(queryHref({ app, sel: n.id }))}">${esc(n.label)}</a>`
    : '';
```
```ts title="packages/devtool/src/client/devtool-pz.client.js"
// Pan / zoom / hover enhancement island (SPEC §4.7 — an `on:visible` bootstrap
// that owns a canvas widget and registers cleanup on ctx.signal, the sanctioned
// pattern for "map instances / observers"). Pure progressive enhancement: the
// server-rendered graph is fully usable with this module absent (selection is
// real <a href> navigation). Loaded on first visibility, never eagerly.

export function Devtool$init(_event, ctx) {
  const root = document.querySelector('[data-pz-root]');
  if (!root || root.__pzInit) return; // idempotent — on:visible may re-fire after morph
  root.__pzInit = true;

  const wrap = root.closest('.canvas-wrap') || root.parentElement;
  const pz = root.querySelector('[data-pz]');
  if (!wrap || !pz) return;

  const signal = ctx && ctx.signal;
  const on = (el, ev, fn, opts) =>
    el.addEventListener(ev, fn, signal ? Object.assign({ signal }, opts || {}) : opts);
  const cssEsc = (s) => String(s).replace(/["\\]/g, '\\$&');

  // ---- transform state ----
  let scale = 1,
    tx = 0,
    ty = 0;
  const MIN = 0.35,
    MAX = 2.6;
  pz.style.transformOrigin = '0 0';
  pz.style.willChange = 'transform';
  const apply = () => {
    pz.style.transform = `translate(${tx}px,${ty}px) scale(${scale})`;
  };

  // take layout over from the JS-off CSS centering
  wrap.style.overflow = 'hidden';
  wrap.style.alignItems = 'flex-start';
  wrap.style.justifyContent = 'flex-start';
  wrap.style.cursor = 'grab';

  const graphW = () => pz.scrollWidth || root.offsetWidth || 1;
  const graphH = () => pz.scrollHeight || root.offsetHeight || 1;

  const fit = () => {
    const w = wrap.clientWidth,
      h = wrap.clientHeight;
    const gW = graphW(),
      gH = graphH();
    let s = Math.min(1, (w - 56) / gW, (h - 56) / gH);
    if (!isFinite(s) || s <= 0) s = 1;
    scale = s;
    tx = Math.max(20, (w - gW * scale) / 2);
    ty = Math.max(20, (h - gH * scale) / 2);
    apply();
  };

  const zoomAround = (cx, cy, next) => {
    const n = Math.min(MAX, Math.max(MIN, next));
    const k = n / scale;
    tx = cx - (cx - tx) * k;
    ty = cy - (cy - ty) * k;
    scale = n;
    apply();
  };

  // ---- wheel zoom toward cursor ----
  on(
    wrap,
    'wheel',
    (e) => {
      e.preventDefault();
      const r = wrap.getBoundingClientRect();
      zoomAround(e.clientX - r.left, e.clientY - r.top, scale * Math.exp(-e.deltaY * 0.0015));
    },
    { passive: false },
  );

  // ---- drag to pan (background only; node links keep working) ----
  let dragging = false,
    sx = 0,
    sy = 0,
    stx = 0,
    sty = 0;
  on(wrap, 'pointerdown', (e) => {
    if (e.button !== 0) return;
    if (e.target.closest('.node, .zoom, button, a')) return;
    dragging = true;
    sx = e.clientX;
    sy = e.clientY;
    stx = tx;
    sty = ty;
    wrap.style.cursor = 'grabbing';
    try {
      wrap.setPointerCapture(e.pointerId);
    } catch {}
  });
  on(wrap, 'pointermove', (e) => {
    if (!dragging) return;
    tx = stx + (e.clientX - sx);
    ty = sty + (e.clientY - sy);
    apply();
  });
  const endDrag = () => {
    dragging = false;
    wrap.style.cursor = 'grab';
  };
  on(wrap, 'pointerup', endDrag);
  on(wrap, 'pointercancel', endDrag);

  // ---- hover highlight (1-hop neighborhood) ----
  let hovered = null;
  const clearHover = () => {
    if (!hovered) return;
    pz.querySelectorAll('.hov').forEach((el) => el.classList.remove('hov'));
    root.classList.remove('hovering');
    hovered = null;
  };
  const setHover = (node) => {
    clearHover();
    hovered = node;
    root.classList.add('hovering');
    node.classList.add('hov');
    const id = node.getAttribute('data-node-id');
    pz.querySelectorAll(`path[data-from="${cssEsc(id)}"], path[data-to="${cssEsc(id)}"]`).forEach(
      (p) => {
        p.classList.add('hov');
        const other =
          p.getAttribute('data-from') === id
            ? p.getAttribute('data-to')
            : p.getAttribute('data-from');
        const n2 = pz.querySelector(`.node[data-node-id="${cssEsc(other)}"]`);
        if (n2) n2.classList.add('hov');
      },
    );
  };
  on(wrap, 'pointerover', (e) => {
    if (dragging) return;
    const node = e.target.closest && e.target.closest('.node[data-node-id]');
    if (node && node !== hovered) setHover(node);
  });
  on(wrap, 'pointerout', (e) => {
    const node = e.target.closest && e.target.closest('.node[data-node-id]');
    if (node && (!e.relatedTarget || !node.contains(e.relatedTarget))) clearHover();
  });

  // ---- zoom buttons + reset ----
  wrap.querySelectorAll('[data-zoom]').forEach((btn) => {
    on(btn, 'click', (e) => {
      e.preventDefault();
      const kind = btn.getAttribute('data-zoom');
      if (kind === 'fit') return fit();
      const r = wrap.getBoundingClientRect();
      zoomAround(r.width / 2, r.height / 2, scale * (kind === 'in' ? 1.25 : 1 / 1.25));
    });
  });
  on(wrap, 'dblclick', (e) => {
    if (!e.target.closest('.node, .zoom')) fit();
  });

  // ---- keyboard a11y: arrows pan, +/- zoom, 0 fits ----
  on(wrap, 'keydown', (e) => {
    const step = 60;
    if (e.key === 'ArrowLeft') {
      tx += step;
      apply();
    } else if (e.key === 'ArrowRight') {
      tx -= step;
      apply();
    } else if (e.key === 'ArrowUp') {
      ty += step;
      apply();
    } else if (e.key === 'ArrowDown') {
      ty -= step;
      apply();
    } else if (e.key === '+' || e.key === '=')
      zoomAround(wrap.clientWidth / 2, wrap.clientHeight / 2, scale * 1.25);
    else if (e.key === '-') zoomAround(wrap.clientWidth / 2, wrap.clientHeight / 2, scale / 1.25);
    else if (e.key === '0') fit();
    else return;
    e.preventDefault();
  });
  wrap.tabIndex = wrap.tabIndex < 0 ? 0 : wrap.tabIndex;

  fit();
  on(window, 'resize', fit);
}
```