# Automation Graph — interactive highlighting Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Сделать карту автоматизации `docs/automation-graph.html` интерактивной: (1) клик по нижней легенде = подсветить только узлы/рёбра этого типа; (2) клик по узлу = подсветить узел + его прямых соседей; (3) при обоих активных переключателях — 3 уровня opacity (focus `1.0` > filter `0.55` > rest `0.15`).

**Architecture:** Все изменения локализованы в одном файле `docs/automation-graph.html` (single-file static, no build pipeline). Новая `SECTION 8: HIGHLIGHTING` обёрнута в IIFE с явным state-объектом `{selectedNode, legendFilter:Set}`. Pre-computed indices (NEIGHBOURS, CONFLICT_ENDPOINTS, CONFLICT_EDGE_TYPE) строятся один раз. Единая функция `applyHighlight()` пересчитывает opacity для всех 73 узлов и ~75 рёбер по правилам из spec §5.1/§5.2. Три существующих handlers расширяются (`network.on('click')`, `#btn-clear`, `#search` input), один новый event listener — делегация клика по `#cat-legend`.

**Tech Stack:** Vanilla JS + vis-network@9.1.9 (через `nodesDS.update()` / `edgesDS.update()`). CSS — solarized-палитра. HTML5 `data-*` атрибуты для метаданных. **Тестов нет** — файл standalone HTML без npm/pest. Verification — manual smoke в Edge browser по 12 сценариям из spec §11.

**Spec:** [docs/superpowers/specs/2026-05-15-graph-interactive-highlighting-design.md](../specs/2026-05-15-graph-interactive-highlighting-design.md) — single source of truth для всех 7 решений brainstorming + правил opacity + точек вставки.

---

## File Structure

Все изменения — в одном файле:

| File | Изменение | Что отвечает |
|---|---|---|
| [docs/automation-graph.html](../../automation-graph.html) | +~110 строк (10 CSS + 0 HTML-байт визуально + 12 data-attr + 90 JS), правка 3 существующих handler'ов | Граф-визуализация + интерактивная подсветка |
| [docs/superpowers/specs/2026-05-15-graph-interactive-highlighting-design.md](../specs/2026-05-15-graph-interactive-highlighting-design.md) | — | Spec (already written, single source of truth) |

**Никаких новых файлов.** Никаких изменений в CLAUDE.md / Pravila / Tooling / реестр (фича внутренняя для карты, не нормативная). Никаких изменений в `db/schema.sql` / `lefthook.yml` / `package.json`.

**Atomic commits map** (Pravila §4.2 — один логический change = один commit):

| Task | Commit subject |
|---|---|
| 1 | `feat(graph): CSS rules for interactive legend (.cat-item hover/active states)` |
| 2 | `feat(graph): add data-filter-key to 12 .cat-item elements` |
| 3 | `feat(graph): SECTION 8 — state + indices + computeOpacity + applyHighlight (infra)` |
| 4 | `feat(graph): legend click delegation — toggle filter + apply highlight` |
| 5 | `feat(graph): network click → selectedNode + toggle on repeat` |
| 6 | `feat(graph): btn-clear + search input integration with highlight state` |
| 7 | (no commit — manual smoke report) |

---

## Task 1: CSS rules for `.cat-item` interactivity

**Files:**

- Modify: `docs/automation-graph.html` (style block, после строки 60)

**Цель:** Добавить визуальные стили для кликабельных элементов нижней легенды — cursor pointer, hover-фон, active-состояние с обводкой. Без JS-логики (это Task 4). После этой задачи легенда **визуально** реагирует на hover мышкой, но клик ещё не делает highlight.

- [ ] **Step 1: Manual smoke before — baseline**

  Открыть `docs/automation-graph.html` в браузере (Edge). Подвести мышь к любому элементу `#cat-legend` (например «Агенты»). **Expected:** курсор не меняется (стрелка), элементы не реагируют на hover.

- [ ] **Step 2: Read existing style block at line 59–60**

  Используя tool Read, прочитать `docs/automation-graph.html` строки 59–61. Подтвердить, что строка 60 содержит `.cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }` и строка 61 — `</style>`.

- [ ] **Step 3: Insert CSS rules after `.cat-dot`**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
      .cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
    </style>

  new_string:
      .cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
      .cat-item {
        cursor: pointer;
        padding: 2px 6px;
        border-radius: 4px;
        transition: background 0.12s, box-shadow 0.12s;
        user-select: none;
      }
      .cat-item:hover { background: rgba(255,255,255,0.05); }
      .cat-item.active {
        background: rgba(253,246,227,0.12);
        box-shadow: inset 0 0 0 1px rgba(253,246,227,0.4);
        color: #fdf6e3;
      }
    </style>
  ```

  **Замечание:** существующее правило `.cat-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #839496; }` (line 59) **остаётся как есть** — мы добавляем второе правило с тем же селектором ниже. CSS-каскад применяет оба, новые поля (cursor, padding, …) не конфликтуют с display/align-items/gap/font-size/color.

- [ ] **Step 4: Manual smoke after — hover reacts**

  Сохранить файл (Edit делает это автоматически), обновить страницу `docs/automation-graph.html` в браузере (Ctrl+F5). Подвести мышь к «Агенты» в нижней легенде. **Expected:**
  - Курсор меняется на pointer (palец).
  - Фон элемента становится слегка светлее (rgba 0.05 — едва заметно, но различимо).
  - При уходе мыши — фон возвращается плавно за 120ms.

- [ ] **Step 5: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): CSS rules for interactive legend (.cat-item hover/active states)"
  ```

---

## Task 2: HTML `data-filter-key` attributes on 12 `.cat-item`

**Files:**

- Modify: `docs/automation-graph.html` (HTML block, строки 108–121)

**Цель:** Добавить `data-filter-key` атрибут на каждый из 12 элементов `.cat-item`. Атрибуты — ключи для логики `legendFilter` (`Set<string>`). После этой задачи в DevTools видны атрибуты, но клик пока не делает ничего (JS-логика — в Task 4).

- [ ] **Step 1: Manual smoke before**

  В DevTools (F12) → Elements → найти `#cat-legend`. **Expected:** 12 `<div class="cat-item">` без `data-*` атрибутов.

- [ ] **Step 2: Replace all 12 `.cat-item` lines atomically**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
  <div id="cat-legend">
    <div class="cat-item"><div class="cat-dot" style="background:#268bd2"></div>Правила</div>
    <div class="cat-item"><div class="cat-dot" style="background:#859900"></div>Плагины</div>
    <div class="cat-item"><div class="cat-dot" style="background:#6c71c4"></div>Скилы Superpowers</div>
    <div class="cat-item"><div class="cat-dot" style="background:#d33682"></div>Скилы проекта</div>
    <div class="cat-item"><div class="cat-dot" style="background:#2aa198"></div>Хуки</div>
    <div class="cat-item"><div class="cat-dot" style="background:#b58900"></div>Агенты</div>
    <div class="cat-item"><div class="cat-dot" style="background:#cb4b16"></div>MCP-серверы</div>
    <div class="cat-item"><div class="cat-dot" style="background:#dc322f"></div>Lefthook jobs</div>
    <div class="cat-item"><div class="cat-dot" style="background:#586e75"></div>Memory files</div>
    <div class="cat-item"><div class="cat-dot" style="background:#ff5f57; border:1px dashed #ff5f57"></div>🔴 Не закрыт правилом</div>
    <div class="cat-item"><div class="cat-dot" style="background:#888888; border:1px dashed #888888"></div>⚫ Возник на практике</div>
    <div class="cat-item"><div class="cat-dot" style="background:#859900; border:1px dashed #859900"></div>🟢 Закрыт правилом</div>
  </div>

  new_string:
  <div id="cat-legend">
    <div class="cat-item" data-filter-key="group:rules"><div class="cat-dot" style="background:#268bd2"></div>Правила</div>
    <div class="cat-item" data-filter-key="group:plugins"><div class="cat-dot" style="background:#859900"></div>Плагины</div>
    <div class="cat-item" data-filter-key="group:skills_sp"><div class="cat-dot" style="background:#6c71c4"></div>Скилы Superpowers</div>
    <div class="cat-item" data-filter-key="group:skills_proj"><div class="cat-dot" style="background:#d33682"></div>Скилы проекта</div>
    <div class="cat-item" data-filter-key="group:hooks"><div class="cat-dot" style="background:#2aa198"></div>Хуки</div>
    <div class="cat-item" data-filter-key="group:agents"><div class="cat-dot" style="background:#b58900"></div>Агенты</div>
    <div class="cat-item" data-filter-key="group:mcp"><div class="cat-dot" style="background:#cb4b16"></div>MCP-серверы</div>
    <div class="cat-item" data-filter-key="group:lefthook"><div class="cat-dot" style="background:#dc322f"></div>Lefthook jobs</div>
    <div class="cat-item" data-filter-key="group:memory"><div class="cat-dot" style="background:#586e75"></div>Memory files</div>
    <div class="cat-item" data-filter-key="conflict:RED"><div class="cat-dot" style="background:#ff5f57; border:1px dashed #ff5f57"></div>🔴 Не закрыт правилом</div>
    <div class="cat-item" data-filter-key="conflict:BLACK"><div class="cat-dot" style="background:#888888; border:1px dashed #888888"></div>⚫ Возник на практике</div>
    <div class="cat-item" data-filter-key="conflict:GREEN"><div class="cat-dot" style="background:#859900; border:1px dashed #859900"></div>🟢 Закрыт правилом</div>
  </div>
  ```

  **NB:** ключи групп узлов точно совпадают с `n.group` в `NODES` массиве (`rules / plugins / skills_sp / skills_proj / hooks / agents / mcp / lefthook / memory`). Ключи конфликтов точно совпадают с ключами `CONFLICT_TYPES` (`RED / BLACK / GREEN`).

- [ ] **Step 3: Manual smoke after**

  Обновить страницу (Ctrl+F5). В DevTools (F12) → Elements → найти `#cat-legend`. **Expected:** все 12 `<div class="cat-item">` имеют атрибут `data-filter-key` с правильным значением. Hover/курсор работают как после Task 1. Клик по элементу всё ещё ничего не делает (это Task 4).

- [ ] **Step 4: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): add data-filter-key to 12 .cat-item elements"
  ```

---

## Task 3: SECTION 8 — state + indices + computations + `applyHighlight` (infra)

**Files:**

- Modify: `docs/automation-graph.html` (после строки 1437 `})();`, перед строкой 1439 `window.addEventListener('DOMContentLoaded', restoreLegendWidth);`)

**Цель:** Создать инфраструктурный слой подсветки: state-объект, 3 pre-computed индекса, функции `computeNodeOpacity` / `computeEdgeOpacity` / `applyHighlight`. После этой задачи код **уже** содержит всю логику, но `applyHighlight()` нигде не вызывается, так что визуально на карте ничего не меняется. В DevTools console можно вручную вызвать `applyHighlight()` — будет no-op (state пуст → idle → opacity 1.0 для всего, что и так).

- [ ] **Step 1: Manual smoke before — confirm insertion point**

  Используя tool Read, прочитать `docs/automation-graph.html` строки 1435–1440. **Expected:** строка 1437 содержит ровно `})();` (закрытие resize IIFE), строка 1438 — пустая, строка 1439 — `window.addEventListener('DOMContentLoaded', restoreLegendWidth);`.

- [ ] **Step 2: Insert SECTION 8 IIFE between resize-IIFE close and DOMContentLoaded**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
  })();

  window.addEventListener('DOMContentLoaded', restoreLegendWidth);
  </script>

  new_string:
  })();

  // ════════════════════════════════════════════════════
  // SECTION 8: HIGHLIGHTING (legend filter + node focus)
  // ════════════════════════════════════════════════════
  const HIGHLIGHT = (function setupHighlight() {
    const FILTER_GROUP_PREFIX = 'group:';
    const FILTER_CONFLICT_PREFIX = 'conflict:';
    const OPACITY_FOCUS = 1.0;
    const OPACITY_FILTER = 0.55;
    const OPACITY_DIM = 0.15;
    const CONFLICT_EDGE_MIN_OPACITY = 0.85;

    const state = {
      selectedNode: null,
      legendFilter: new Set(),
    };

    // ── Pre-computed indices ──────────────────────────
    const NODES_BY_ID = new Map();
    const NEIGHBOURS = new Map();
    const CONFLICT_ENDPOINTS = { RED: new Set(), BLACK: new Set(), GREEN: new Set() };
    const CONFLICT_EDGE_TYPE = new Map();

    NODES.forEach(n => {
      NODES_BY_ID.set(n.id, n);
      if (!NEIGHBOURS.has(n.id)) NEIGHBOURS.set(n.id, new Set());
    });

    edgesDS.get().forEach(edge => {
      // Both directions — Q5; conflict edges count as connections — Q6
      if (NEIGHBOURS.has(edge.from)) NEIGHBOURS.get(edge.from).add(edge.to);
      if (NEIGHBOURS.has(edge.to))   NEIGHBOURS.get(edge.to).add(edge.from);
      if (edge.dashes && edge.color && edge.color.color) {
        for (const t of ['RED', 'BLACK', 'GREEN']) {
          if (CONFLICT_TYPES[t].color === edge.color.color) {
            CONFLICT_ENDPOINTS[t].add(edge.from);
            CONFLICT_ENDPOINTS[t].add(edge.to);
            CONFLICT_EDGE_TYPE.set(edge.id, t);
            break;
          }
        }
      }
    });

    // ── Opacity computations ──────────────────────────
    function computeNodeOpacity(nodeId) {
      // Row 1: focus
      if (state.selectedNode !== null) {
        if (state.selectedNode === nodeId) return OPACITY_FOCUS;
        const neigh = NEIGHBOURS.get(state.selectedNode);
        if (neigh && neigh.has(nodeId)) return OPACITY_FOCUS;
      }
      // Row 2: idle
      if (state.legendFilter.size === 0 && state.selectedNode === null) return OPACITY_FOCUS;
      // Row 3: in filter?
      const node = NODES_BY_ID.get(nodeId);
      let inFilter = false;
      if (node && state.legendFilter.has(FILTER_GROUP_PREFIX + node.group)) inFilter = true;
      if (!inFilter) {
        for (const t of ['RED', 'BLACK', 'GREEN']) {
          if (state.legendFilter.has(FILTER_CONFLICT_PREFIX + t) && CONFLICT_ENDPOINTS[t].has(nodeId)) {
            inFilter = true;
            break;
          }
        }
      }
      if (inFilter) return state.selectedNode ? OPACITY_FILTER : OPACITY_FOCUS;
      // Row 4: everything else
      return OPACITY_DIM;
    }

    function computeEdgeOpacity(edge) {
      const fromO = computeNodeOpacity(edge.from);
      const toO   = computeNodeOpacity(edge.to);
      const baseline = Math.min(fromO, toO);
      // Conflict edge directly selected via 🔴/⚫/🟢 in filter — boost to ≥0.85
      const ctype = CONFLICT_EDGE_TYPE.get(edge.id);
      if (ctype && state.legendFilter.has(FILTER_CONFLICT_PREFIX + ctype)) {
        return Math.max(CONFLICT_EDGE_MIN_OPACITY, baseline);
      }
      return baseline;
    }

    function applyHighlight() {
      const nodeUpdates = NODES.map(n => ({
        id: n.id,
        opacity: computeNodeOpacity(n.id),
      }));
      const edgeUpdates = edgesDS.get().map(e => ({
        id: e.id,
        color: Object.assign({}, e.color || {}, { opacity: computeEdgeOpacity(e) }),
      }));
      nodesDS.update(nodeUpdates);
      edgesDS.update(edgeUpdates);
    }

    // ── State manipulators ────────────────────────────
    function toggleFilter(key) {
      if (state.legendFilter.has(key)) state.legendFilter.delete(key);
      else state.legendFilter.add(key);
    }

    function setSelectedNode(id) {
      if (state.selectedNode === id) state.selectedNode = null; // toggle on repeat
      else state.selectedNode = id;
    }

    function clearAll() {
      state.selectedNode = null;
      state.legendFilter.clear();
    }

    function updateLegendVisuals() {
      document.querySelectorAll('#cat-legend .cat-item').forEach(item => {
        const key = item.dataset.filterKey;
        if (!key) return;
        if (state.legendFilter.has(key)) item.classList.add('active');
        else item.classList.remove('active');
      });
    }

    // Expose API (closes over state)
    return {
      applyHighlight,
      toggleFilter,
      setSelectedNode,
      clearAll,
      updateLegendVisuals,
      state, // exposed for debug only
    };
  })();

  window.addEventListener('DOMContentLoaded', restoreLegendWidth);
  </script>
  ```

- [ ] **Step 3: Manual smoke after — API callable, no visual change**

  Обновить страницу (Ctrl+F5). Открыть DevTools (F12) → Console. Выполнить:

  ```js
  HIGHLIGHT.state
  ```

  **Expected:** `{ selectedNode: null, legendFilter: Set(0) {} }`

  Выполнить:

  ```js
  HIGHLIGHT.applyHighlight()
  ```

  **Expected:** возврат `undefined` (функция void), на карте визуальных изменений нет (все узлы остаются 1.0 — idle path).

  Выполнить:

  ```js
  HIGHLIGHT.state.selectedNode = 'pravila'; HIGHLIGHT.applyHighlight()
  ```

  **Expected:** на карте узел `pravila` и его 3 соседа (`claude_md / psr_v1 / superpowers`) остаются яркими; остальные 69 узлов и их рёбра приглушаются до 0.15. Это проверка через прямой манипул state — event-handlers ещё не привязаны.

  Сбросить вручную:

  ```js
  HIGHLIGHT.clearAll(); HIGHLIGHT.applyHighlight()
  ```

  **Expected:** всё возвращается к 1.0.

- [ ] **Step 4: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): SECTION 8 — state + indices + computeOpacity + applyHighlight (infra)"
  ```

---

## Task 4: Legend click delegation → toggle + apply

**Files:**

- Modify: `docs/automation-graph.html` (в SECTION 8 IIFE добавить event listener; точная вставка — перед `return { ... }`)

**Цель:** Привязать клик по `.cat-item` к `toggleFilter` + `applyHighlight` + `updateLegendVisuals`. После этой задачи **первая фича (кликабельная легенда) полностью работает**: клик включает/выключает тип, multi-select работает.

- [ ] **Step 1: Manual smoke before**

  Обновить страницу. Кликнуть «Агенты» в нижней легенде. **Expected:** ничего не происходит (handler ещё не привязан). DevTools console: `HIGHLIGHT.state.legendFilter` остаётся `Set(0) {}`.

- [ ] **Step 2: Insert delegation listener inside the IIFE**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
      function updateLegendVisuals() {
        document.querySelectorAll('#cat-legend .cat-item').forEach(item => {
          const key = item.dataset.filterKey;
          if (!key) return;
          if (state.legendFilter.has(key)) item.classList.add('active');
          else item.classList.remove('active');
        });
      }

      // Expose API (closes over state)
      return {

  new_string:
      function updateLegendVisuals() {
        document.querySelectorAll('#cat-legend .cat-item').forEach(item => {
          const key = item.dataset.filterKey;
          if (!key) return;
          if (state.legendFilter.has(key)) item.classList.add('active');
          else item.classList.remove('active');
        });
      }

      // ── Legend click delegation ───────────────────────
      document.getElementById('cat-legend').addEventListener('click', e => {
        const item = e.target.closest('.cat-item');
        if (!item || !item.dataset.filterKey) return;
        toggleFilter(item.dataset.filterKey);
        applyHighlight();
        updateLegendVisuals();
      });

      // Expose API (closes over state)
      return {
  ```

- [ ] **Step 3: Manual smoke after — feature 1 live**

  Обновить страницу (Ctrl+F5). Прогнать 5 микро-сценариев:

  **3.1.** Кликнуть «Агенты». **Expected:** 11 agent-узлов остаются 1.0, остальные 62 → 0.15. `.cat-item` «Агенты» получает обводку (`.active`).

  **3.2.** Кликнуть «MCP-серверы». **Expected:** 18 узлов (11 agents + 7 MCP) на 1.0; 55 — на 0.15. Оба `.cat-item` с `.active`. `HIGHLIGHT.state.legendFilter` = `Set(2) { 'group:agents', 'group:mcp' }`.

  **3.3.** Кликнуть «Агенты» снова. **Expected:** «Агенты» снимает `.active`; 7 MCP остаются 1.0, остальные 66 — 0.15.

  **3.4.** Кликнуть 🔴 «Не закрыт правилом». **Expected:** 7 MCP остаются 1.0, плюс 4 endpoint-узла RED-конфликтов (`sk_rls / ag_rls / hookify_plugin / hk_pre_claude`) на 1.0. 2 RED-ребра на 1.0 (boosted ≥0.85 через `computeEdgeOpacity`). Прочие 62 узла — 0.15.

  **3.5.** Кликнуть «MCP-серверы» и 🔴 ещё раз (toggle off обоих). **Expected:** `HIGHLIGHT.state.legendFilter` = `Set(0) {}`, все 73 узла и ~75 рёбер возвращаются к 1.0 (idle path в `computeNodeOpacity`).

- [ ] **Step 4: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): legend click delegation — toggle filter + apply highlight"
  ```

---

## Task 5: `network.on('click')` extension → selectedNode + toggle on repeat

**Files:**

- Modify: `docs/automation-graph.html` (строки 1307–1315 — расширить существующий handler)

**Цель:** Привязать клик по узлу графа к `setSelectedNode` + `applyHighlight`. Сохранить существующее поведение `showNodeLegend` / `showEdgeLegend` / panel-hide на пустой клик. Добавить toggle-семантику: повторный клик по тому же узлу снимает selection. После этой задачи **вторая фича полностью работает** (узел + соседи подсвечиваются), и комбинация с фильтром легенды работает автоматически (3 уровня opacity управляются единой `computeNodeOpacity`).

- [ ] **Step 1: Manual smoke before**

  Обновить страницу. Кликнуть узел `pravila` в центре графа. **Expected:** правая `#legend-panel` показывает детали (существующая логика `showNodeLegend`), но opacity на графе **не меняется** (highlight ещё не привязан).

- [ ] **Step 2: Replace the network click handler at lines 1307–1315**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
  network.on('click', params => {
    if (params.nodes.length === 1) {
      showNodeLegend(params.nodes[0]);
    } else if (params.edges.length === 1) {
      showEdgeLegend(params.edges[0]);
    } else if (params.nodes.length === 0 && params.edges.length === 0) {
      document.getElementById('legend-panel').classList.remove('visible');
    }
  });

  new_string:
  network.on('click', params => {
    if (params.nodes.length === 1) {
      const id = params.nodes[0];
      HIGHLIGHT.setSelectedNode(id);
      HIGHLIGHT.applyHighlight();
      // Right panel still shows details of the clicked node (last-clicked, even after toggle-off)
      showNodeLegend(id);
    } else if (params.edges.length === 1) {
      showEdgeLegend(params.edges[0]);
    } else if (params.nodes.length === 0 && params.edges.length === 0) {
      HIGHLIGHT.state.selectedNode = null;
      HIGHLIGHT.applyHighlight();
      document.getElementById('legend-panel').classList.remove('visible');
    }
  });
  ```

  **NB:** `legendFilter` НЕ сбрасывается при пустом клике (spec §6, строка «empty click»). Это сохраняет контекст «у меня выбраны Агенты, я кликнул в пустоту = focus снят, фильтр Агенты остаётся».

- [ ] **Step 3: Manual smoke after — feature 2 live + combination**

  Обновить страницу (Ctrl+F5). Прогнать 6 микро-сценариев:

  **3.1. Single node focus:** Кликнуть `pravila`. **Expected:** 4 узла (`pravila` + 3 соседа `claude_md / psr_v1 / superpowers`) на 1.0; остальные 69 — на 0.15. Все рёбра между фокус-узлами — 1.0. Правая panel показывает детали `pravila`.

  **3.2. Toggle off node:** Кликнуть `pravila` снова. **Expected:** все 73 узла → 1.0, фильтр пуст (`HIGHLIGHT.state.selectedNode === null`). Правая panel остаётся видимой (показывает последний выбранный узел).

  **3.3. Empty-area click:** Кликнуть в пустое место графа. **Expected:** `selectedNode === null` (если был set), `applyHighlight()` пересчитывает, panel скрывается (существующая логика).

  **3.4. Conflict neighbour (Q6):** Кликнуть `ag_pest`. **Expected:** 4 узла яркие — `ag_pest` + `mcp_redis` (через и обычное `E()`, и `CONFLICT(BLACK)` ребро — оба добавляют mcp_redis в NEIGHBOURS) + `claude_md` (E('claude_md', 'ag_pest', ...)) + `mem_env`. Оба ребра `ag_pest`↔`mcp_redis` — яркие (1.0, baseline).

  **3.5. Combination — 3 levels of opacity:** Из состояния (3.4) кликнуть «Агенты» в нижней легенде. **Expected:** 4 фокус-узла (`ag_pest` + соседи) на 1.0; 10 других агентов на 0.55; остальные на 0.15. Это первое визуальное подтверждение Q3 (3 уровня opacity).

  **3.6. Reverse order — filter first, then node:** Сбросить (`btn-clear`). Кликнуть «Агенты + MCP». Кликнуть `ag_pest`. **Expected (spec §11 сценарий 9):** `ag_pest` + 3 соседа на 1.0 (4 узла); 10 других агентов + 6 других MCP на 0.55 (16 узлов); прочие 53 узла на 0.15.

- [ ] **Step 4: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): network click → selectedNode + toggle on repeat"
  ```

---

## Task 6: `#btn-clear` + `#search` input integration

**Files:**

- Modify: `docs/automation-graph.html` (строки 1326–1348 — search handler; строки 1378–1383 — btn-clear handler)

**Цель:** Расширить `#btn-clear` для сброса обоих стейтов (selectedNode + legendFilter) и нашего CSS-класса `.active`. Расширить `#search` input handler для last-wins сброса нашего state (search — отдельный режим). После этой задачи все 6 строк таблицы handlers из spec §6 интегрированы; фича полностью завершена.

- [ ] **Step 1: Manual smoke before — btn-clear gap + search gap**

  Установить состояние: кликнуть «Агенты» + кликнуть `ag_pest` (комбинация 3 уровня opacity). Кликнуть `#btn-clear`. **Expected gap:** opacity сбрасывается (через existing `nodesDS.update` в btn-clear), но `.cat-item` «Агенты» **остаётся с `.active`**-классом (не сбрасывается); `HIGHLIGHT.state.legendFilter` всё ещё содержит `group:agents`. Это баг, который Task 6 закрывает.

  Установить состояние: кликнуть «MCP» + ввести `pest` в поле поиска. **Expected gap:** search-логика подсветила matches (через свой opacity update), но `HIGHLIGHT.state` остался прежним (`legendFilter` содержит `mcp`). Это конфликт состояний, который Task 6 закрывает.

- [ ] **Step 2: Extend `#btn-clear` handler at lines 1378–1383**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
  document.getElementById('btn-clear').addEventListener('click', () => {
    document.getElementById('search').value = '';
    nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
    document.getElementById('legend-panel').classList.remove('visible');
    highlightedNode = null;
  });

  new_string:
  document.getElementById('btn-clear').addEventListener('click', () => {
    document.getElementById('search').value = '';
    HIGHLIGHT.clearAll();
    HIGHLIGHT.updateLegendVisuals();
    HIGHLIGHT.applyHighlight();
    nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2 }))); // reset borderWidth used by search-highlight
    document.getElementById('legend-panel').classList.remove('visible');
    highlightedNode = null;
  });
  ```

  **NB:** Существующий `nodesDS.update(... opacity: 1.0)` заменён на `applyHighlight()` (который сам выставит 1.0 для всего в idle-состоянии после `clearAll`). Отдельный `nodesDS.update` для `borderWidth: 2` оставлен — search использует `borderWidth: 5` для matches, его тоже надо сбросить.

- [ ] **Step 3: Extend `#search` input handler at lines 1326–1348**

  Использовать tool Edit на `docs/automation-graph.html`:

  ```
  old_string:
  document.getElementById('search').addEventListener('input', function () {
    const q = this.value.trim().toLowerCase();
    if (!q) {
      nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
      highlightedNode = null;
      return;
    }
    const matches = NODES.filter(n => n.label.toLowerCase().includes(q));
    const updates = NODES.map(n => {
      const match = matches.some(m => m.id === n.id);
      return {
        id: n.id,
        borderWidth: match ? 5 : 1,
        opacity: match ? 1.0 : 0.25,
      };
    });
    nodesDS.update(updates);
    if (matches.length === 1) {
      network.focus(matches[0].id, { scale: 1.4, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
      showNodeLegend(matches[0].id);
      highlightedNode = matches[0].id;
    }
  });

  new_string:
  document.getElementById('search').addEventListener('input', function () {
    // Search is a separate mode — last-wins over highlight state
    HIGHLIGHT.clearAll();
    HIGHLIGHT.updateLegendVisuals();
    const q = this.value.trim().toLowerCase();
    if (!q) {
      nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
      highlightedNode = null;
      return;
    }
    const matches = NODES.filter(n => n.label.toLowerCase().includes(q));
    const updates = NODES.map(n => {
      const match = matches.some(m => m.id === n.id);
      return {
        id: n.id,
        borderWidth: match ? 5 : 1,
        opacity: match ? 1.0 : 0.25,
      };
    });
    nodesDS.update(updates);
    if (matches.length === 1) {
      network.focus(matches[0].id, { scale: 1.4, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
      showNodeLegend(matches[0].id);
      highlightedNode = matches[0].id;
    }
  });
  ```

  **Изменение:** добавлены 2 строки в начале — `HIGHLIGHT.clearAll(); HIGHLIGHT.updateLegendVisuals();`. Остальное идентично оригиналу.

- [ ] **Step 4: Manual smoke after — gap closures**

  Обновить страницу (Ctrl+F5). Прогнать 4 микро-сценария:

  **4.1.** Кликнуть «Агенты» + `ag_pest` → `#btn-clear`. **Expected:** все 73 узла на 1.0; `.cat-item` «Агенты» без `.active`; `HIGHLIGHT.state` = `{ selectedNode: null, legendFilter: Set(0) {} }`; panel скрыта; поле search пусто.

  **4.2.** Кликнуть «MCP» (multi-select). Ввести `pest` в search. **Expected:** `.cat-item` «MCP» теряет `.active` сразу при первом keystroke; нечёткие 73 узла - matches — приглушены до 0.25 (search-логика); matches яркие с borderWidth 5; `HIGHLIGHT.state.legendFilter` = `Set(0) {}` (last-wins).

  **4.3.** Очистить search (Backspace до пустого). **Expected:** все узлы → 1.0, borderWidth → 2 (existing `if (!q)` branch).

  **4.4.** Без search — кликнуть «Агенты» → `#btn-clear` → кликнуть `ag_pest`. **Expected:** последовательность работает чисто — после btn-clear состояние полностью идемпотентно к idle.

- [ ] **Step 5: Commit**

  ```bash
  git add docs/automation-graph.html
  git commit -m "feat(graph): btn-clear + search input integration with highlight state"
  ```

---

## Task 7: Manual smoke checklist — все 12 сценариев из spec §11

**Files:** ничего не меняется (только проверка).

**Цель:** Пройти все 12 manual smoke сценариев из spec §11 (single source of truth) и зафиксировать результат. Если все 12 проходят без console-errors — фича готова к claim «complete». Если какой-то сценарий падает (например, vis-network warning'и на `opacity: 1.0` — см. spec §12.2 «open question») — задокументировать и решить через план B.

- [ ] **Step 1: Open `docs/automation-graph.html` in clean state**

  В Edge: `Ctrl+F5` (hard reload). DevTools (F12) → Console (очистить кнопкой 🚫). Никаких других вкладок не нужно.

- [ ] **Step 2: Run all 12 smoke scenarios from spec §11**

  Прогнать **последовательно** все 12 сценариев из [spec §11](../specs/2026-05-15-graph-interactive-highlighting-design.md):

  1. **Idle:** Открыть → все 73 узла + ~75 рёбер на 1.0, легенда без `.active`.
  2. **Single legend (group):** Клик «Агенты» → 11 ярких, 62 → 0.15, `.active` есть.
  3. **Toggle off:** Клик «Агенты» снова → всё 1.0, `.active` снят.
  4. **Multi-select group:** Клик «Агенты» + «MCP» → 18 ярких, 55 → 0.15.
  5. **Multi-select drop one:** Клик «Агенты» из (4) → 7 MCP яркие, остальные 66 → 0.15.
  6. **Single legend (conflict):** Клик 🔴 → 4 endpoint-узла + 2 RED-ребра яркие, 69 узлов → 0.15.
  7. **Node focus alone:** Клик `pravila` → 4 узла (`pravila` + `claude_md / psr_v1 / superpowers`) яркие, 69 → 0.15; правая panel открыта.
  8. **Node toggle off:** Клик `pravila` снова → 73 узла на 1.0; panel **остаётся** видимой (spec §12.1 — это ожидаемое поведение).
  9. **Node + filter combined:** Из (4) клик `ag_pest` → 4 фокус-узла на 1.0; 16 в фильтре на 0.55; 53 прочих на 0.15.
  10. **Search override:** Из (9) ввести `pest` в поиск → `HIGHLIGHT.state` сбрасывается, search работает по-старому.
  11. **Clear all:** `#btn-clear` → 73 узла 1.0, `.active` снят со всех, panel скрыта, search пуст.
  12. **Conflict edge type detection:** Клик ⚫ → 4 BLACK-endpoint (`mcp_pw / sk_parallel / ag_pest / mcp_redis`) яркие, 2 BLACK-ребра яркие, остальное 0.15.

  По каждому сценарию записать в отчёт (см. Step 3): PASS / FAIL + observed behaviour.

- [ ] **Step 3: Write smoke report**

  Создать рабочий отчёт (плоский текст, без коммита):

  ```
  Smoke report — 2026-05-15, graph-interactive-highlighting

  Scenario 1 (idle):                PASS / FAIL — <observed>
  Scenario 2 (single group):        PASS / FAIL — <observed>
  Scenario 3 (toggle off):          PASS / FAIL — <observed>
  Scenario 4 (multi-select group):  PASS / FAIL — <observed>
  Scenario 5 (drop one):            PASS / FAIL — <observed>
  Scenario 6 (single conflict):     PASS / FAIL — <observed>
  Scenario 7 (node focus alone):    PASS / FAIL — <observed>
  Scenario 8 (node toggle off):     PASS / FAIL — <observed>
  Scenario 9 (node + filter):       PASS / FAIL — <observed>
  Scenario 10 (search override):    PASS / FAIL — <observed>
  Scenario 11 (clear all):          PASS / FAIL — <observed>
  Scenario 12 (conflict edge type): PASS / FAIL — <observed>

  Console errors/warnings: <count + first line>
  Notes:                  <e.g. spec §12.2 vis-network edge.color.opacity behaviour>
  ```

- [ ] **Step 4: If any scenario FAILs — diagnose via `superpowers:systematic-debugging`**

  При FAIL:
  1. Минимум 3 гипотезы причины.
  2. Falsify каждую через DevTools console (e.g. `HIGHLIGHT.state`, `nodesDS.get('ag_pest').opacity`, `edgesDS.get().filter(e => e.dashes).map(e => e.color)` etc).
  3. Только после reproducible root cause — править код в новом atomic commit.

- [ ] **Step 5: If all 12 PASS — invoke `superpowers:verification-before-completion`**

  Перед claim'ом «фича готова» обязательно через verification skill (экономия 0% жёсткое требование). Запустить:
  - Smoke 12/12 PASS (Step 2 result)
  - 0 console errors / warnings
  - `git status` clean (все 6 commits сделаны)
  - `git log --oneline -7` показывает 6 task-коммитов в правильном порядке + предыдущий main HEAD

---

## Self-Review

Прошёлся по spec секциям 0–12, сверил с задачами выше:

| Spec section | Coverage | Notes |
|---|---|---|
| 0. Context | — | Контекст, не покрытие |
| 1. Цели и не-цели | Task 1–6 покрывают все 4 цели; «не-цели» формально невозможно «не реализовать» — гарантируется отсутствием соответствующих задач | ✓ |
| 2. Решения brainstorming | Q1 (соседи 1-го уровня) — Task 3 (`NEIGHBOURS` index, both directions). Q2 (multi-select) — Task 4 (`toggleFilter`). Q3 (3 уровня) — Task 3 (`computeNodeOpacity` row 3 conditional). Q4 (конфликтные рёбра) — Task 3 (`CONFLICT_EDGE_TYPE` + `computeEdgeOpacity` boost). Q5 (both) — Task 3 (`NEIGHBOURS` симметричный). Q6 (конфликт=связь) — Task 3 (тот же NEIGHBOURS, добавляются все рёбра вкл. dashed). Approach (IIFE) — Task 3. | ✓ |
| 3. Архитектура | Task 3 — вся §3 одним блоком | ✓ |
| 4. Pre-computed indices | Task 3 — все 4 индекса | ✓ |
| 5. Правила opacity | Task 3 — `computeNodeOpacity` + `computeEdgeOpacity` + `applyHighlight` | ✓ |
| 6. Event handlers | Task 4 (legend click), Task 5 (network click), Task 6 (btn-clear + search) | ✓ — все 6 строк §6 таблицы покрыты |
| 7. UI-метки в легенде | Task 1 (CSS), Task 2 (data-filter-key), Task 4 (делегация) | ✓ |
| 8. Интеграция с существующим кодом | Task 6 явно покрывает btn-reset/freeze/unfreeze «без изменений» (мы их не трогаем) | ✓ |
| 9. Производительность | Не требует реализации — pre-computed indices уже в Task 3 | ✓ |
| 10. Error handling | Не требует реализации — pre-conditions в Task 3 уже corrected (NODES_BY_ID.get, NEIGHBOURS.has) | ✓ |
| 11. Manual smoke checklist | Task 7 — все 12 сценариев | ✓ |
| 12. Open questions | Task 7 Step 4 покрывает §12.2 (vis-network edge.color.opacity) через systematic-debugging; §12.1 (panel-hide) explicitly accepted в Task 5 Step 3.2; §12.3 (rapid clicks) обнаружится в Task 4/5 smoke если будет проблема | ✓ |

**Placeholder scan:** искал `TBD / TODO / fill in / similar to / handle edge cases / appropriate error`. Найдено: 0. Все коды задач — конкретные `old_string` / `new_string` либо явные блоки с готовым кодом.

**Type consistency:** проверил identifiers, используемые в нескольких задачах:

- `HIGHLIGHT.applyHighlight()` — Task 3 определяет (returns from IIFE), Task 4/5/6 вызывают — ✓
- `HIGHLIGHT.clearAll()` — Task 3 определяет, Task 6 вызывает — ✓ (не `clearFullLayers` или другое)
- `HIGHLIGHT.toggleFilter(key)` — Task 3 определяет, Task 4 вызывает с `item.dataset.filterKey` — ✓
- `HIGHLIGHT.setSelectedNode(id)` — Task 3 определяет (с toggle-семантикой), Task 5 вызывает — ✓
- `HIGHLIGHT.updateLegendVisuals()` — Task 3 определяет, Task 4 + Task 6 вызывают — ✓
- `HIGHLIGHT.state` — exposed в Task 3 для debug, Task 5 строка 3 «empty click» прямо манипулирует `HIGHLIGHT.state.selectedNode = null` (документировано в spec §6 строка empty click) — ✓
- `NEIGHBOURS / CONFLICT_ENDPOINTS / CONFLICT_EDGE_TYPE / NODES_BY_ID` — private в IIFE Task 3, наружу не утекают — ✓
- `data-filter-key` префиксы: Task 2 HTML использует `group:` / `conflict:`; Task 3 константы `FILTER_GROUP_PREFIX = 'group:'` / `FILTER_CONFLICT_PREFIX = 'conflict:'` — ✓
- `.cat-item.active` CSS-класс: Task 1 CSS-правило, Task 3 `updateLegendVisuals` add/remove — ✓

Конфликтов / дрейфа имён не найдено.

---

**Plan complete.** Saved to [docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md](2026-05-15-graph-interactive-highlighting.md).
