8a22cc45c5
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>
751 lines
44 KiB
Markdown
751 lines
44 KiB
Markdown
# 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 — `</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, строки 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-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` строки 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. Выполнить:
|
||
|
||
```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 — <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).
|