diff --git a/cspell-words.txt b/cspell-words.txt index 0c4ad16f..1e41aa1c 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1146,3 +1146,9 @@ skreview pdd иммутабельны федокруг + +# Automation Graph iter3 (2026-05-15) — UK-spelling neighbour vars + graph algo abbreviations +NEIGHBOURS +neighbour +BFS +DFS diff --git a/docs/smoke-2026-05-15-graph-highlighting-scenario2.png b/docs/smoke-2026-05-15-graph-highlighting-scenario2.png new file mode 100644 index 00000000..3967940d Binary files /dev/null and b/docs/smoke-2026-05-15-graph-highlighting-scenario2.png differ diff --git a/docs/smoke-2026-05-15-graph-highlighting-scenario9.png b/docs/smoke-2026-05-15-graph-highlighting-scenario9.png new file mode 100644 index 00000000..f415f913 Binary files /dev/null and b/docs/smoke-2026-05-15-graph-highlighting-scenario9.png differ diff --git a/docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md b/docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md new file mode 100644 index 00000000..12b68329 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md @@ -0,0 +1,750 @@ +# 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). diff --git a/docs/superpowers/specs/2026-05-15-graph-interactive-highlighting-design.md b/docs/superpowers/specs/2026-05-15-graph-interactive-highlighting-design.md new file mode 100644 index 00000000..647d6439 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-graph-interactive-highlighting-design.md @@ -0,0 +1,334 @@ +--- +title: "Automation Graph — interactive highlighting (clickable legend + node neighbour highlight)" +date: 2026-05-15 +author: Claude (через superpowers:brainstorming) +status: design — awaiting user review +related: + - docs/automation-graph.html (artifact, 1442 строк) + - docs/superpowers/specs/2026-05-14-automation-graph-iter2-design.md (iter2 spec — закрыт `2ad35ca`) + - docs/superpowers/plans/2026-05-14-automation-graph-iter2.md (iter2 plan — закрыт) + - memory/project_automation_map.md (термин «карта») +--- + +# Automation Graph — interactive highlighting (iter3) + +## 0. Context + +`docs/automation-graph.html` — single-file интерактивный граф автоматизации Лидерры на `vis-network@9.1.9` (73 узла / 9 групп категорий + 3 типа конфликтных рёбер / ~80 рёбер из них 8 конфликтных / radial-sector layout). После итераций iter1 (refactor 14.05.2026) и iter2 (resize + plain-language + 3-color conflicts + edge legend, 14.05.2026 commit `2ad35ca`) карта используется ежедневно как ориентир по системе автоматизации. + +Заказчик Дмитрий — нетехнический владелец проекта. После недели использования зафиксировал **две связанные фичи** для iter3: + +1. **Интерактивная нижняя легенда** (`#cat-legend`, 12 элементов): клик по типу = подсветить только узлы/рёбра этого типа. +2. **Выделение соседей при клике по узлу**: связанные узлы яркие, остальные приглушены. + +Brainstorming-цикл прошёл 15.05.2026 — 6 clarifying-вопросов о семантике (scope связей / multi-select / комбинация / типы конфликтов / направление рёбер / конфликты как связи) + 1 о подходе к коду. Все 7 решений приняты заказчиком. + +## 1. Цели и не-цели + +**Цели:** + +1. Сделать нижнюю легенду (`#cat-legend`, 12 элементов: 9 групп узлов + 3 типа конфликтных рёбер) кликабельной с multi-select-семантикой. +2. При клике по узлу подсветить узел + его прямых соседей (соседи 1-го уровня в любом направлении, включая конфликтные рёбра). +3. Когда оба переключателя активны одновременно — три уровня opacity (`focus 1.0 > filter 0.55 > rest 0.15`). +4. Сохранить существующее поведение `#search` / `btn-freeze` / `btn-unfreeze` / `btn-reset` / `btn-clear` без регрессий. + +**Не-цели:** + +- Не менять radial-sector layout или координаты узлов. +- Не менять списки `NODES` / `EDGES` / `NODE_DETAILS` / `EDGE_DETAILS`. +- Не добавлять транзитивный обход графа (BFS/DFS) — только соседи 1-го уровня (Q1). +- Не добавлять модификаторы клавиатуры (Shift/Ctrl) — multi-select работает обычными кликами (Q2). +- Не вводить тесты `vitest`/`pest` — карта остаётся single-file static HTML без build pipeline. Проверка — manual smoke в браузере. + +## 2. Решения brainstorming (single source of truth) + +| # | Вопрос | Решение | +|---|---|---| +| Q1 | Что значит «связанные элементы» при клике на узел? | **Соседи 1-го уровня** (без транзитивного обхода). | +| Q2 | Как выбирать типы в нижней легенде? | **Multi-select кликами**, без модификаторов. Кнопка «Снять выделение» очищает. | +| Q3 | Комбинация фильтр-легенды + выделение узла | **3 уровня opacity**: focus `1.0` > filter `0.55` > rest `0.15`. | +| Q4 | Клик по 🔴/⚫/🟢 в легенде | **Подсветить конфликтные рёбра выбранного типа + их endpoint-узлы.** Сами конфликтные рёбра ≥ 0.85 даже при частичном фокусе. | +| Q5 | Направление рёбер при выделении узла | **Both** — и in (`B→A`), и out (`A→B`) делают `B` соседом `A`. | +| Q6 | Конфликтное (пунктирное) ребро = связь? | **Да** — конфликт считается связью; при клике по узлу endpoint конфликта попадает в соседство. | +| Approach | Код-стиль фичи в HTML-файле | **IIFE с явным state-объектом** (~90 строк JS + ~10 CSS). | + +## 3. Архитектура + +### 3.1. Размещение в файле + +Новая `SECTION 8: HIGHLIGHTING` в конце `