# 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 — ``. - [ ] **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; } 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; } ``` **Замечание:** существующее правило `.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`). После этой задачи в DevTools видны атрибуты, но клик пока не делает ничего (JS-логика — в Task 4). - [ ] **Step 1: Manual smoke before** В DevTools (F12) → Elements → найти `#cat-legend`. **Expected:** 12 `
` без `data-*` атрибутов. - [ ] **Step 2: Replace all 12 `.cat-item` lines atomically** Использовать tool Edit на `docs/automation-graph.html`: ``` old_string:
Правила
Плагины
Скилы Superpowers
Скилы проекта
Хуки
Агенты
MCP-серверы
Lefthook jobs
Memory files
🔴 Не закрыт правилом
⚫ Возник на практике
🟢 Закрыт правилом
new_string:
Правила
Плагины
Скилы Superpowers
Скилы проекта
Хуки
Агенты
MCP-серверы
Lefthook jobs
Memory files
🔴 Не закрыт правилом
⚫ Возник на практике
🟢 Закрыт правилом
``` **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 `
` имеют атрибут `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); 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); ``` - [ ] **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 — Scenario 2 (single group): PASS / FAIL — Scenario 3 (toggle off): PASS / FAIL — Scenario 4 (multi-select group): PASS / FAIL — Scenario 5 (drop one): PASS / FAIL — Scenario 6 (single conflict): PASS / FAIL — Scenario 7 (node focus alone): PASS / FAIL — Scenario 8 (node toggle off): PASS / FAIL — Scenario 9 (node + filter): PASS / FAIL — Scenario 10 (search override): PASS / FAIL — Scenario 11 (clear all): PASS / FAIL — Scenario 12 (conflict edge type): PASS / FAIL — Console errors/warnings: Notes: ``` - [ ] **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).