Files
brain/docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md
T

44 KiB
Raw Blame History

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 — single source of truth для всех 7 решений brainstorming + правил opacity + точек вставки.


File Structure

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

File Изменение Что отвечает
docs/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 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 5960

    Используя 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

    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, строки 108121)

Цель: Добавить 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

    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 строки 14351440. 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. Выполнить:

    HIGHLIGHT.state
    

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

    Выполнить:

    HIGHLIGHT.applyHighlight()
    

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

    Выполнить:

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

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

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

    HIGHLIGHT.clearAll(); HIGHLIGHT.applyHighlight()
    

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

  • Step 4: Commit

    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

    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 13071315

    Использовать 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_pestmcp_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

    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 (строки 13261348 — search handler; строки 13781383 — 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 13781383

    Использовать 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 13261348

    Использовать 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

    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:

    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.