Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44 KiB
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 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
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-itemlines 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})();, перед строкой 1439window.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. Выполнить:
HIGHLIGHT.stateExpected:
{ 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 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
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 сбрасывается (через existingnodesDS.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-clearhandler 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
#searchinput 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
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.htmlin clean stateВ Edge:
Ctrl+F5(hard reload). DevTools (F12) → Console (очистить кнопкой 🚫). Никаких других вкладок не нужно. -
Step 2: Run all 12 smoke scenarios from spec §11
Прогнать последовательно все 12 сценариев из spec §11:
- Idle: Открыть → все 73 узла + ~75 рёбер на 1.0, легенда без
.active. - Single legend (group): Клик «Агенты» → 11 ярких, 62 → 0.15,
.activeесть. - Toggle off: Клик «Агенты» снова → всё 1.0,
.activeснят. - Multi-select group: Клик «Агенты» + «MCP» → 18 ярких, 55 → 0.15.
- Multi-select drop one: Клик «Агенты» из (4) → 7 MCP яркие, остальные 66 → 0.15.
- Single legend (conflict): Клик 🔴 → 4 endpoint-узла + 2 RED-ребра яркие, 69 узлов → 0.15.
- Node focus alone: Клик
pravila→ 4 узла (pravila+claude_md / psr_v1 / superpowers) яркие, 69 → 0.15; правая panel открыта. - Node toggle off: Клик
pravilaснова → 73 узла на 1.0; panel остаётся видимой (spec §12.1 — это ожидаемое поведение). - Node + filter combined: Из (4) клик
ag_pest→ 4 фокус-узла на 1.0; 16 в фильтре на 0.55; 53 прочих на 0.15. - Search override: Из (9) ввести
pestв поиск →HIGHLIGHT.stateсбрасывается, search работает по-старому. - Clear all:
#btn-clear→ 73 узла 1.0,.activeснят со всех, panel скрыта, search пуст. - 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.
- Idle: Открыть → все 73 узла + ~75 рёбер на 1.0, легенда без
-
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:
- Минимум 3 гипотезы причины.
- Falsify каждую через DevTools console (e.g.
HIGHLIGHT.state,nodesDS.get('ag_pest').opacity,edgesDS.get().filter(e => e.dashes).map(e => e.color)etc). - Только после 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 statusclean (все 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.activeCSS-класс: Task 1 CSS-правило, Task 3updateLegendVisualsadd/remove — ✓
Конфликтов / дрейфа имён не найдено.
Plan complete. Saved to docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md.