Files
portal/docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md
T
Дмитрий 8a22cc45c5 docs(graph): iter3 closure — spec + plan + smoke evidence + cspell terms
iter3 «Automation Graph — interactive highlighting» закрыт.
8 implementation commits ef88435..f0d3d49 (6 feat + 2 fix).
Smoke 12/12 PASS via Playwright (raw JSON + 2 screenshots).
markdownlint/cspell/lychee — clean. Final cross-commit review: APPROVED.

+spec/plan: docs/superpowers/{specs,plans}/2026-05-15-graph-*.md
+smoke evidence: docs/smoke-2026-05-15-graph-highlighting-scenario{2,9}.png
+cspell: NEIGHBOURS / neighbour / BFS / DFS (iter3 vocabulary)

iter4 backlog (non-blocking): I-1 falsy-coercion line 1531, dead var
highlightedNode, SECTION 6 comment update, optional rAF-throttle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:04:57 +03:00

751 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 5960**
Используя tool Read, прочитать `docs/automation-graph.html` строки 59–61. Подтвердить, что строка 60 содержит `.cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }` и строка 61 — `</style>`.
- [ ] **Step 3: Insert CSS rules after `.cat-dot`**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
.cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
</style>
new_string:
.cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.cat-item {
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.12s, box-shadow 0.12s;
user-select: none;
}
.cat-item:hover { background: rgba(255,255,255,0.05); }
.cat-item.active {
background: rgba(253,246,227,0.12);
box-shadow: inset 0 0 0 1px rgba(253,246,227,0.4);
color: #fdf6e3;
}
</style>
```
**Замечание:** существующее правило `.cat-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #839496; }` (line 59) **остаётся как есть** — мы добавляем второе правило с тем же селектором ниже. CSS-каскад применяет оба, новые поля (cursor, padding, …) не конфликтуют с display/align-items/gap/font-size/color.
- [ ] **Step 4: Manual smoke after — hover reacts**
Сохранить файл (Edit делает это автоматически), обновить страницу `docs/automation-graph.html` в браузере (Ctrl+F5). Подвести мышь к «Агенты» в нижней легенде. **Expected:**
- Курсор меняется на pointer (palец).
- Фон элемента становится слегка светлее (rgba 0.05 — едва заметно, но различимо).
- При уходе мыши — фон возвращается плавно за 120ms.
- [ ] **Step 5: Commit**
```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, строки 108121)
**Цель:** Добавить `data-filter-key` атрибут на каждый из 12 элементов `.cat-item`. Атрибуты — ключи для логики `legendFilter` (`Set<string>`). После этой задачи в DevTools видны атрибуты, но клик пока не делает ничего (JS-логика — в Task 4).
- [ ] **Step 1: Manual smoke before**
В DevTools (F12) → Elements → найти `#cat-legend`. **Expected:** 12 `<div class="cat-item">` без `data-*` атрибутов.
- [ ] **Step 2: Replace all 12 `.cat-item` lines atomically**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
<div id="cat-legend">
<div class="cat-item"><div class="cat-dot" style="background:#268bd2"></div>Правила</div>
<div class="cat-item"><div class="cat-dot" style="background:#859900"></div>Плагины</div>
<div class="cat-item"><div class="cat-dot" style="background:#6c71c4"></div>Скилы Superpowers</div>
<div class="cat-item"><div class="cat-dot" style="background:#d33682"></div>Скилы проекта</div>
<div class="cat-item"><div class="cat-dot" style="background:#2aa198"></div>Хуки</div>
<div class="cat-item"><div class="cat-dot" style="background:#b58900"></div>Агенты</div>
<div class="cat-item"><div class="cat-dot" style="background:#cb4b16"></div>MCP-серверы</div>
<div class="cat-item"><div class="cat-dot" style="background:#dc322f"></div>Lefthook jobs</div>
<div class="cat-item"><div class="cat-dot" style="background:#586e75"></div>Memory files</div>
<div class="cat-item"><div class="cat-dot" style="background:#ff5f57; border:1px dashed #ff5f57"></div>🔴 Не закрыт правилом</div>
<div class="cat-item"><div class="cat-dot" style="background:#888888; border:1px dashed #888888"></div>⚫ Возник на практике</div>
<div class="cat-item"><div class="cat-dot" style="background:#859900; border:1px dashed #859900"></div>🟢 Закрыт правилом</div>
</div>
new_string:
<div id="cat-legend">
<div class="cat-item" data-filter-key="group:rules"><div class="cat-dot" style="background:#268bd2"></div>Правила</div>
<div class="cat-item" data-filter-key="group:plugins"><div class="cat-dot" style="background:#859900"></div>Плагины</div>
<div class="cat-item" data-filter-key="group:skills_sp"><div class="cat-dot" style="background:#6c71c4"></div>Скилы Superpowers</div>
<div class="cat-item" data-filter-key="group:skills_proj"><div class="cat-dot" style="background:#d33682"></div>Скилы проекта</div>
<div class="cat-item" data-filter-key="group:hooks"><div class="cat-dot" style="background:#2aa198"></div>Хуки</div>
<div class="cat-item" data-filter-key="group:agents"><div class="cat-dot" style="background:#b58900"></div>Агенты</div>
<div class="cat-item" data-filter-key="group:mcp"><div class="cat-dot" style="background:#cb4b16"></div>MCP-серверы</div>
<div class="cat-item" data-filter-key="group:lefthook"><div class="cat-dot" style="background:#dc322f"></div>Lefthook jobs</div>
<div class="cat-item" data-filter-key="group:memory"><div class="cat-dot" style="background:#586e75"></div>Memory files</div>
<div class="cat-item" data-filter-key="conflict:RED"><div class="cat-dot" style="background:#ff5f57; border:1px dashed #ff5f57"></div>🔴 Не закрыт правилом</div>
<div class="cat-item" data-filter-key="conflict:BLACK"><div class="cat-dot" style="background:#888888; border:1px dashed #888888"></div>⚫ Возник на практике</div>
<div class="cat-item" data-filter-key="conflict:GREEN"><div class="cat-dot" style="background:#859900; border:1px dashed #859900"></div>🟢 Закрыт правилом</div>
</div>
```
**NB:** ключи групп узлов точно совпадают с `n.group` в `NODES` массиве (`rules / plugins / skills_sp / skills_proj / hooks / agents / mcp / lefthook / memory`). Ключи конфликтов точно совпадают с ключами `CONFLICT_TYPES` (`RED / BLACK / GREEN`).
- [ ] **Step 3: Manual smoke after**
Обновить страницу (Ctrl+F5). В DevTools (F12) → Elements → найти `#cat-legend`. **Expected:** все 12 `<div class="cat-item">` имеют атрибут `data-filter-key` с правильным значением. Hover/курсор работают как после Task 1. Клик по элементу всё ещё ничего не делает (это Task 4).
- [ ] **Step 4: Commit**
```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` строки 14351440. **Expected:** строка 1437 содержит ровно `})();` (закрытие resize IIFE), строка 1438 — пустая, строка 1439 — `window.addEventListener('DOMContentLoaded', restoreLegendWidth);`.
- [ ] **Step 2: Insert SECTION 8 IIFE between resize-IIFE close and DOMContentLoaded**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
})();
window.addEventListener('DOMContentLoaded', restoreLegendWidth);
</script>
new_string:
})();
// ════════════════════════════════════════════════════
// SECTION 8: HIGHLIGHTING (legend filter + node focus)
// ════════════════════════════════════════════════════
const HIGHLIGHT = (function setupHighlight() {
const FILTER_GROUP_PREFIX = 'group:';
const FILTER_CONFLICT_PREFIX = 'conflict:';
const OPACITY_FOCUS = 1.0;
const OPACITY_FILTER = 0.55;
const OPACITY_DIM = 0.15;
const CONFLICT_EDGE_MIN_OPACITY = 0.85;
const state = {
selectedNode: null,
legendFilter: new Set(),
};
// ── Pre-computed indices ──────────────────────────
const NODES_BY_ID = new Map();
const NEIGHBOURS = new Map();
const CONFLICT_ENDPOINTS = { RED: new Set(), BLACK: new Set(), GREEN: new Set() };
const CONFLICT_EDGE_TYPE = new Map();
NODES.forEach(n => {
NODES_BY_ID.set(n.id, n);
if (!NEIGHBOURS.has(n.id)) NEIGHBOURS.set(n.id, new Set());
});
edgesDS.get().forEach(edge => {
// Both directions — Q5; conflict edges count as connections — Q6
if (NEIGHBOURS.has(edge.from)) NEIGHBOURS.get(edge.from).add(edge.to);
if (NEIGHBOURS.has(edge.to)) NEIGHBOURS.get(edge.to).add(edge.from);
if (edge.dashes && edge.color && edge.color.color) {
for (const t of ['RED', 'BLACK', 'GREEN']) {
if (CONFLICT_TYPES[t].color === edge.color.color) {
CONFLICT_ENDPOINTS[t].add(edge.from);
CONFLICT_ENDPOINTS[t].add(edge.to);
CONFLICT_EDGE_TYPE.set(edge.id, t);
break;
}
}
}
});
// ── Opacity computations ──────────────────────────
function computeNodeOpacity(nodeId) {
// Row 1: focus
if (state.selectedNode !== null) {
if (state.selectedNode === nodeId) return OPACITY_FOCUS;
const neigh = NEIGHBOURS.get(state.selectedNode);
if (neigh && neigh.has(nodeId)) return OPACITY_FOCUS;
}
// Row 2: idle
if (state.legendFilter.size === 0 && state.selectedNode === null) return OPACITY_FOCUS;
// Row 3: in filter?
const node = NODES_BY_ID.get(nodeId);
let inFilter = false;
if (node && state.legendFilter.has(FILTER_GROUP_PREFIX + node.group)) inFilter = true;
if (!inFilter) {
for (const t of ['RED', 'BLACK', 'GREEN']) {
if (state.legendFilter.has(FILTER_CONFLICT_PREFIX + t) && CONFLICT_ENDPOINTS[t].has(nodeId)) {
inFilter = true;
break;
}
}
}
if (inFilter) return state.selectedNode ? OPACITY_FILTER : OPACITY_FOCUS;
// Row 4: everything else
return OPACITY_DIM;
}
function computeEdgeOpacity(edge) {
const fromO = computeNodeOpacity(edge.from);
const toO = computeNodeOpacity(edge.to);
const baseline = Math.min(fromO, toO);
// Conflict edge directly selected via 🔴/⚫/🟢 in filter — boost to ≥0.85
const ctype = CONFLICT_EDGE_TYPE.get(edge.id);
if (ctype && state.legendFilter.has(FILTER_CONFLICT_PREFIX + ctype)) {
return Math.max(CONFLICT_EDGE_MIN_OPACITY, baseline);
}
return baseline;
}
function applyHighlight() {
const nodeUpdates = NODES.map(n => ({
id: n.id,
opacity: computeNodeOpacity(n.id),
}));
const edgeUpdates = edgesDS.get().map(e => ({
id: e.id,
color: Object.assign({}, e.color || {}, { opacity: computeEdgeOpacity(e) }),
}));
nodesDS.update(nodeUpdates);
edgesDS.update(edgeUpdates);
}
// ── State manipulators ────────────────────────────
function toggleFilter(key) {
if (state.legendFilter.has(key)) state.legendFilter.delete(key);
else state.legendFilter.add(key);
}
function setSelectedNode(id) {
if (state.selectedNode === id) state.selectedNode = null; // toggle on repeat
else state.selectedNode = id;
}
function clearAll() {
state.selectedNode = null;
state.legendFilter.clear();
}
function updateLegendVisuals() {
document.querySelectorAll('#cat-legend .cat-item').forEach(item => {
const key = item.dataset.filterKey;
if (!key) return;
if (state.legendFilter.has(key)) item.classList.add('active');
else item.classList.remove('active');
});
}
// Expose API (closes over state)
return {
applyHighlight,
toggleFilter,
setSelectedNode,
clearAll,
updateLegendVisuals,
state, // exposed for debug only
};
})();
window.addEventListener('DOMContentLoaded', restoreLegendWidth);
</script>
```
- [ ] **Step 3: Manual smoke after — API callable, no visual change**
Обновить страницу (Ctrl+F5). Открыть DevTools (F12) → Console. Выполнить:
```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 13071315**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
network.on('click', params => {
if (params.nodes.length === 1) {
showNodeLegend(params.nodes[0]);
} else if (params.edges.length === 1) {
showEdgeLegend(params.edges[0]);
} else if (params.nodes.length === 0 && params.edges.length === 0) {
document.getElementById('legend-panel').classList.remove('visible');
}
});
new_string:
network.on('click', params => {
if (params.nodes.length === 1) {
const id = params.nodes[0];
HIGHLIGHT.setSelectedNode(id);
HIGHLIGHT.applyHighlight();
// Right panel still shows details of the clicked node (last-clicked, even after toggle-off)
showNodeLegend(id);
} else if (params.edges.length === 1) {
showEdgeLegend(params.edges[0]);
} else if (params.nodes.length === 0 && params.edges.length === 0) {
HIGHLIGHT.state.selectedNode = null;
HIGHLIGHT.applyHighlight();
document.getElementById('legend-panel').classList.remove('visible');
}
});
```
**NB:** `legendFilter` НЕ сбрасывается при пустом клике (spec §6, строка «empty click»). Это сохраняет контекст «у меня выбраны Агенты, я кликнул в пустоту = focus снят, фильтр Агенты остаётся».
- [ ] **Step 3: Manual smoke after — feature 2 live + combination**
Обновить страницу (Ctrl+F5). Прогнать 6 микро-сценариев:
**3.1. Single node focus:** Кликнуть `pravila`. **Expected:** 4 узла (`pravila` + 3 соседа `claude_md / psr_v1 / superpowers`) на 1.0; остальные 69 — на 0.15. Все рёбра между фокус-узлами — 1.0. Правая panel показывает детали `pravila`.
**3.2. Toggle off node:** Кликнуть `pravila` снова. **Expected:** все 73 узла → 1.0, фильтр пуст (`HIGHLIGHT.state.selectedNode === null`). Правая panel остаётся видимой (показывает последний выбранный узел).
**3.3. Empty-area click:** Кликнуть в пустое место графа. **Expected:** `selectedNode === null` (если был set), `applyHighlight()` пересчитывает, panel скрывается (существующая логика).
**3.4. Conflict neighbour (Q6):** Кликнуть `ag_pest`. **Expected:** 4 узла яркие — `ag_pest` + `mcp_redis` (через и обычное `E()`, и `CONFLICT(BLACK)` ребро — оба добавляют mcp_redis в NEIGHBOURS) + `claude_md` (E('claude_md', 'ag_pest', ...)) + `mem_env`. Оба ребра `ag_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` (строки 13261348 — search handler; строки 13781383 — btn-clear handler)
**Цель:** Расширить `#btn-clear` для сброса обоих стейтов (selectedNode + legendFilter) и нашего CSS-класса `.active`. Расширить `#search` input handler для last-wins сброса нашего state (search — отдельный режим). После этой задачи все 6 строк таблицы handlers из spec §6 интегрированы; фича полностью завершена.
- [ ] **Step 1: Manual smoke before — btn-clear gap + search gap**
Установить состояние: кликнуть «Агенты» + кликнуть `ag_pest` (комбинация 3 уровня opacity). Кликнуть `#btn-clear`. **Expected gap:** opacity сбрасывается (через existing `nodesDS.update` в btn-clear), но `.cat-item` «Агенты» **остаётся с `.active`**-классом (не сбрасывается); `HIGHLIGHT.state.legendFilter` всё ещё содержит `group:agents`. Это баг, который Task 6 закрывает.
Установить состояние: кликнуть «MCP» + ввести `pest` в поле поиска. **Expected gap:** search-логика подсветила matches (через свой opacity update), но `HIGHLIGHT.state` остался прежним (`legendFilter` содержит `mcp`). Это конфликт состояний, который Task 6 закрывает.
- [ ] **Step 2: Extend `#btn-clear` handler at lines 13781383**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
document.getElementById('btn-clear').addEventListener('click', () => {
document.getElementById('search').value = '';
nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
document.getElementById('legend-panel').classList.remove('visible');
highlightedNode = null;
});
new_string:
document.getElementById('btn-clear').addEventListener('click', () => {
document.getElementById('search').value = '';
HIGHLIGHT.clearAll();
HIGHLIGHT.updateLegendVisuals();
HIGHLIGHT.applyHighlight();
nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2 }))); // reset borderWidth used by search-highlight
document.getElementById('legend-panel').classList.remove('visible');
highlightedNode = null;
});
```
**NB:** Существующий `nodesDS.update(... opacity: 1.0)` заменён на `applyHighlight()` (который сам выставит 1.0 для всего в idle-состоянии после `clearAll`). Отдельный `nodesDS.update` для `borderWidth: 2` оставлен — search использует `borderWidth: 5` для matches, его тоже надо сбросить.
- [ ] **Step 3: Extend `#search` input handler at lines 13261348**
Использовать tool Edit на `docs/automation-graph.html`:
```
old_string:
document.getElementById('search').addEventListener('input', function () {
const q = this.value.trim().toLowerCase();
if (!q) {
nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
highlightedNode = null;
return;
}
const matches = NODES.filter(n => n.label.toLowerCase().includes(q));
const updates = NODES.map(n => {
const match = matches.some(m => m.id === n.id);
return {
id: n.id,
borderWidth: match ? 5 : 1,
opacity: match ? 1.0 : 0.25,
};
});
nodesDS.update(updates);
if (matches.length === 1) {
network.focus(matches[0].id, { scale: 1.4, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
showNodeLegend(matches[0].id);
highlightedNode = matches[0].id;
}
});
new_string:
document.getElementById('search').addEventListener('input', function () {
// Search is a separate mode — last-wins over highlight state
HIGHLIGHT.clearAll();
HIGHLIGHT.updateLegendVisuals();
const q = this.value.trim().toLowerCase();
if (!q) {
nodesDS.update(NODES.map(n => ({ id: n.id, borderWidth: 2, opacity: 1.0 })));
highlightedNode = null;
return;
}
const matches = NODES.filter(n => n.label.toLowerCase().includes(q));
const updates = NODES.map(n => {
const match = matches.some(m => m.id === n.id);
return {
id: n.id,
borderWidth: match ? 5 : 1,
opacity: match ? 1.0 : 0.25,
};
});
nodesDS.update(updates);
if (matches.length === 1) {
network.focus(matches[0].id, { scale: 1.4, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
showNodeLegend(matches[0].id);
highlightedNode = matches[0].id;
}
});
```
**Изменение:** добавлены 2 строки в начале — `HIGHLIGHT.clearAll(); HIGHLIGHT.updateLegendVisuals();`. Остальное идентично оригиналу.
- [ ] **Step 4: Manual smoke after — gap closures**
Обновить страницу (Ctrl+F5). Прогнать 4 микро-сценария:
**4.1.** Кликнуть «Агенты» + `ag_pest` → `#btn-clear`. **Expected:** все 73 узла на 1.0; `.cat-item` «Агенты» без `.active`; `HIGHLIGHT.state` = `{ selectedNode: null, legendFilter: Set(0) {} }`; panel скрыта; поле search пусто.
**4.2.** Кликнуть «MCP» (multi-select). Ввести `pest` в search. **Expected:** `.cat-item` «MCP» теряет `.active` сразу при первом keystroke; нечёткие 73 узла - matches — приглушены до 0.25 (search-логика); matches яркие с borderWidth 5; `HIGHLIGHT.state.legendFilter` = `Set(0) {}` (last-wins).
**4.3.** Очистить search (Backspace до пустого). **Expected:** все узлы → 1.0, borderWidth → 2 (existing `if (!q)` branch).
**4.4.** Без search — кликнуть «Агенты» → `#btn-clear` → кликнуть `ag_pest`. **Expected:** последовательность работает чисто — после btn-clear состояние полностью идемпотентно к idle.
- [ ] **Step 5: Commit**
```bash
git add docs/automation-graph.html
git commit -m "feat(graph): btn-clear + search input integration with highlight state"
```
---
## Task 7: Manual smoke checklist — все 12 сценариев из spec §11
**Files:** ничего не меняется (только проверка).
**Цель:** Пройти все 12 manual smoke сценариев из spec §11 (single source of truth) и зафиксировать результат. Если все 12 проходят без console-errors — фича готова к claim «complete». Если какой-то сценарий падает (например, vis-network warning'и на `opacity: 1.0` — см. spec §12.2 «open question») — задокументировать и решить через план B.
- [ ] **Step 1: Open `docs/automation-graph.html` in clean state**
В Edge: `Ctrl+F5` (hard reload). DevTools (F12) → Console (очистить кнопкой 🚫). Никаких других вкладок не нужно.
- [ ] **Step 2: Run all 12 smoke scenarios from spec §11**
Прогнать **последовательно** все 12 сценариев из [spec §11](../specs/2026-05-15-graph-interactive-highlighting-design.md):
1. **Idle:** Открыть → все 73 узла + ~75 рёбер на 1.0, легенда без `.active`.
2. **Single legend (group):** Клик «Агенты» → 11 ярких, 62 → 0.15, `.active` есть.
3. **Toggle off:** Клик «Агенты» снова → всё 1.0, `.active` снят.
4. **Multi-select group:** Клик «Агенты» + «MCP» → 18 ярких, 55 → 0.15.
5. **Multi-select drop one:** Клик «Агенты» из (4) → 7 MCP яркие, остальные 66 → 0.15.
6. **Single legend (conflict):** Клик 🔴 → 4 endpoint-узла + 2 RED-ребра яркие, 69 узлов → 0.15.
7. **Node focus alone:** Клик `pravila` → 4 узла (`pravila` + `claude_md / psr_v1 / superpowers`) яркие, 69 → 0.15; правая panel открыта.
8. **Node toggle off:** Клик `pravila` снова → 73 узла на 1.0; panel **остаётся** видимой (spec §12.1 — это ожидаемое поведение).
9. **Node + filter combined:** Из (4) клик `ag_pest` → 4 фокус-узла на 1.0; 16 в фильтре на 0.55; 53 прочих на 0.15.
10. **Search override:** Из (9) ввести `pest` в поиск → `HIGHLIGHT.state` сбрасывается, search работает по-старому.
11. **Clear all:** `#btn-clear` → 73 узла 1.0, `.active` снят со всех, panel скрыта, search пуст.
12. **Conflict edge type detection:** Клик ⚫ → 4 BLACK-endpoint (`mcp_pw / sk_parallel / ag_pest / mcp_redis`) яркие, 2 BLACK-ребра яркие, остальное 0.15.
По каждому сценарию записать в отчёт (см. Step 3): PASS / FAIL + observed behaviour.
- [ ] **Step 3: Write smoke report**
Создать рабочий отчёт (плоский текст, без коммита):
```
Smoke report — 2026-05-15, graph-interactive-highlighting
Scenario 1 (idle): PASS / FAIL — <observed>
Scenario 2 (single group): PASS / FAIL — <observed>
Scenario 3 (toggle off): PASS / FAIL — <observed>
Scenario 4 (multi-select group): PASS / FAIL — <observed>
Scenario 5 (drop one): PASS / FAIL — <observed>
Scenario 6 (single conflict): PASS / FAIL — <observed>
Scenario 7 (node focus alone): PASS / FAIL — <observed>
Scenario 8 (node toggle off): PASS / FAIL — <observed>
Scenario 9 (node + filter): PASS / FAIL — <observed>
Scenario 10 (search override): PASS / FAIL — <observed>
Scenario 11 (clear all): PASS / FAIL — <observed>
Scenario 12 (conflict edge type): PASS / FAIL — <observed>
Console errors/warnings: <count + first line>
Notes: <e.g. spec §12.2 vis-network edge.color.opacity behaviour>
```
- [ ] **Step 4: If any scenario FAILs — diagnose via `superpowers:systematic-debugging`**
При FAIL:
1. Минимум 3 гипотезы причины.
2. Falsify каждую через DevTools console (e.g. `HIGHLIGHT.state`, `nodesDS.get('ag_pest').opacity`, `edgesDS.get().filter(e => e.dashes).map(e => e.color)` etc).
3. Только после reproducible root cause — править код в новом atomic commit.
- [ ] **Step 5: If all 12 PASS — invoke `superpowers:verification-before-completion`**
Перед claim'ом «фича готова» обязательно через verification skill (экономия 0% жёсткое требование). Запустить:
- Smoke 12/12 PASS (Step 2 result)
- 0 console errors / warnings
- `git status` clean (все 6 commits сделаны)
- `git log --oneline -7` показывает 6 task-коммитов в правильном порядке + предыдущий main HEAD
---
## Self-Review
Прошёлся по spec секциям 0–12, сверил с задачами выше:
| Spec section | Coverage | Notes |
|---|---|---|
| 0. Context | — | Контекст, не покрытие |
| 1. Цели и не-цели | Task 1–6 покрывают все 4 цели; «не-цели» формально невозможно «не реализовать» — гарантируется отсутствием соответствующих задач | ✓ |
| 2. Решения brainstorming | Q1 (соседи 1-го уровня) — Task 3 (`NEIGHBOURS` index, both directions). Q2 (multi-select) — Task 4 (`toggleFilter`). Q3 (3 уровня) — Task 3 (`computeNodeOpacity` row 3 conditional). Q4 (конфликтные рёбра) — Task 3 (`CONFLICT_EDGE_TYPE` + `computeEdgeOpacity` boost). Q5 (both) — Task 3 (`NEIGHBOURS` симметричный). Q6 (конфликт=связь) — Task 3 (тот же NEIGHBOURS, добавляются все рёбра вкл. dashed). Approach (IIFE) — Task 3. | ✓ |
| 3. Архитектура | Task 3 — вся §3 одним блоком | ✓ |
| 4. Pre-computed indices | Task 3 — все 4 индекса | ✓ |
| 5. Правила opacity | Task 3 — `computeNodeOpacity` + `computeEdgeOpacity` + `applyHighlight` | ✓ |
| 6. Event handlers | Task 4 (legend click), Task 5 (network click), Task 6 (btn-clear + search) | ✓ — все 6 строк §6 таблицы покрыты |
| 7. UI-метки в легенде | Task 1 (CSS), Task 2 (data-filter-key), Task 4 (делегация) | ✓ |
| 8. Интеграция с существующим кодом | Task 6 явно покрывает btn-reset/freeze/unfreeze «без изменений» (мы их не трогаем) | ✓ |
| 9. Производительность | Не требует реализации — pre-computed indices уже в Task 3 | ✓ |
| 10. Error handling | Не требует реализации — pre-conditions в Task 3 уже corrected (NODES_BY_ID.get, NEIGHBOURS.has) | ✓ |
| 11. Manual smoke checklist | Task 7 — все 12 сценариев | ✓ |
| 12. Open questions | Task 7 Step 4 покрывает §12.2 (vis-network edge.color.opacity) через systematic-debugging; §12.1 (panel-hide) explicitly accepted в Task 5 Step 3.2; §12.3 (rapid clicks) обнаружится в Task 4/5 smoke если будет проблема | ✓ |
**Placeholder scan:** искал `TBD / TODO / fill in / similar to / handle edge cases / appropriate error`. Найдено: 0. Все коды задач — конкретные `old_string` / `new_string` либо явные блоки с готовым кодом.
**Type consistency:** проверил identifiers, используемые в нескольких задачах:
- `HIGHLIGHT.applyHighlight()` — Task 3 определяет (returns from IIFE), Task 4/5/6 вызывают — ✓
- `HIGHLIGHT.clearAll()` — Task 3 определяет, Task 6 вызывает — ✓ (не `clearFullLayers` или другое)
- `HIGHLIGHT.toggleFilter(key)` — Task 3 определяет, Task 4 вызывает с `item.dataset.filterKey` — ✓
- `HIGHLIGHT.setSelectedNode(id)` — Task 3 определяет (с toggle-семантикой), Task 5 вызывает — ✓
- `HIGHLIGHT.updateLegendVisuals()` — Task 3 определяет, Task 4 + Task 6 вызывают — ✓
- `HIGHLIGHT.state` — exposed в Task 3 для debug, Task 5 строка 3 «empty click» прямо манипулирует `HIGHLIGHT.state.selectedNode = null` (документировано в spec §6 строка empty click) — ✓
- `NEIGHBOURS / CONFLICT_ENDPOINTS / CONFLICT_EDGE_TYPE / NODES_BY_ID` — private в IIFE Task 3, наружу не утекают — ✓
- `data-filter-key` префиксы: Task 2 HTML использует `group:` / `conflict:`; Task 3 константы `FILTER_GROUP_PREFIX = 'group:'` / `FILTER_CONFLICT_PREFIX = 'conflict:'` — ✓
- `.cat-item.active` CSS-класс: Task 1 CSS-правило, Task 3 `updateLegendVisuals` add/remove — ✓
Конфликтов / дрейфа имён не найдено.
---
**Plan complete.** Saved to [docs/superpowers/plans/2026-05-15-graph-interactive-highlighting.md](2026-05-15-graph-interactive-highlighting.md).