# Interactive Connections Graph — 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:** Добавить в `hooks-skills-plugins-map.html` интерактивную force-directed диаграмму на D3.js v7 с 50 узлами, 52 рёбрами, drag/click/hover/filter взаимодействием.

**Architecture:** Новая HTML-секция §X «Связи — interactive map» с SVG-контейнером, D3.js v7 из CDN, vanilla JS для interactivity, vintage-blueprint aesthetic (наследуется от существующих стилей).

**Tech Stack:** D3.js v7 (CDN), SVG, vanilla JS, CSS variables (--paper, --ink, --blueprint, --amber, --rust, --sage из существующего файла).

**Spec reference:** [`2026-05-10-connections-graph-design.md`](../specs/2026-05-10-connections-graph-design.md) (385 строк, 10 секций).

---

## File Structure

### Модифицируется

| File | Что меняется |
|---|---|
| `docs/visualizations/hooks-skills-plugins-map.html` | Renumber §X→§XI, новая §X секция с graph (~260 строк добавки в существующие 2240) |

### Не создаётся

Никаких новых файлов — всё inline в существующий HTML.

---

## Tasks

### Task 1: Renumber existing §X (Practical Actions) → §XI

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` (1 строка изменена)

- [ ] **Step 1: Edit section-num value X → XI**

Use Edit tool. Найти:

```html
      <div class="section-num display-i">X</div>
      <h2 class="section-title">Что вы можете сделать</h2>
```

Заменить на:

```html
      <div class="section-num display-i">XI</div>
      <h2 class="section-title">Что вы можете сделать</h2>
```

- [ ] **Step 2: Verify единственный X→XI выполнен**

```bash
grep -nE '<div class="section-num display-i">(X|XI)<' \
  "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: одна строка с XI (другие — I-IX, плюс будем добавлять X в Task 3).

---

### Task 2: Add D3 CDN script + graph CSS

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — `<head>` (D3 script) + `<style>` блок (CSS)

- [ ] **Step 1: Add D3 CDN script tag после Google Fonts link**

Use Edit tool. Найти:

```html
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,200..900,0..100,0..1;1,9..144,200..900,0..100,0..1&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
<style>
```

Заменить на:

```html
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,200..900,0..100,0..1;1,9..144,200..900,0..100,0..1&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js" defer></script>
<style>
```

- [ ] **Step 2: Add graph CSS перед PAGE LOAD ANIMATION block**

Use Edit tool. Найти:

```css
/* ============================================================
   PAGE LOAD ANIMATION
   ============================================================ */
@keyframes rise {
```

Заменить на:

```css
/* ============================================================
   CONNECTIONS GRAPH (§X)
   ============================================================ */
.graph-controls {
  margin-top: 32px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 16px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.graph-filters {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.graph-filter {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 12px;
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  transition: background 200ms, color 200ms;
}

.graph-filter.active {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}

.graph-filter:hover { background: var(--paper-shade); }
.graph-filter.active:hover { background: var(--ink-soft); }

.graph-reset {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 14px;
  border: 1px solid var(--rust);
  background: var(--paper);
  color: var(--rust);
  cursor: pointer;
}
.graph-reset:hover { background: var(--rust); color: var(--paper); }

.graph-container {
  margin-top: 24px;
  display: grid;
  grid-template-columns: 1fr 280px;
  gap: 24px;
  align-items: start;
}

.graph-svg-wrap {
  border: 1px solid var(--rule);
  background: var(--paper-shade);
  background-image:
    linear-gradient(rgba(14, 34, 53, 0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(14, 34, 53, 0.03) 1px, transparent 1px);
  background-size: 24px 24px;
  overflow: hidden;
  min-height: 700px;
}

.graph-svg {
  width: 100%;
  height: 700px;
  display: block;
  cursor: default;
}

.graph-sidebar {
  border: 1px solid var(--rule);
  background: var(--paper);
  padding: 20px 18px;
  font-size: 0.88rem;
  min-height: 200px;
  position: relative;
}

.graph-sidebar[hidden] { display: none; }

.graph-sidebar-empty {
  color: var(--ink-fade);
  font-style: italic;
  font-family: 'Fraunces', serif;
}

.graph-sidebar-title {
  font-family: 'Fraunces', serif;
  font-weight: 500;
  font-size: 1.15rem;
  margin-bottom: 4px;
  line-height: 1.15;
}

.graph-sidebar-badge {
  display: inline-block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 2px 8px;
  background: var(--ink);
  color: var(--paper);
  margin-bottom: 12px;
}

.graph-sidebar-section {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px dashed var(--rule);
}

.graph-sidebar-section-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-fade);
  margin-bottom: 8px;
}

.graph-conn-list {
  list-style: none;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  line-height: 1.7;
  color: var(--ink-soft);
}
.graph-conn-list li b { color: var(--rust); font-weight: 600; }

.graph-close {
  position: absolute;
  top: 12px;
  right: 14px;
  border: none;
  background: transparent;
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.2rem;
  cursor: pointer;
  color: var(--ink-fade);
}
.graph-close:hover { color: var(--rust); }

.graph-legend {
  margin-top: 24px;
  padding: 20px 24px;
  background: var(--paper);
  border: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.graph-legend-section h4 {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-fade);
  margin-bottom: 12px;
  font-weight: 500;
}

.graph-legend-item {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
  font-size: 0.82rem;
  color: var(--ink-soft);
}
.graph-legend-swatch {
  width: 16px;
  height: 16px;
  border: 1.5px solid var(--ink);
  flex-shrink: 0;
}
.graph-legend-line {
  width: 28px;
  height: 0;
  border-top: 2px solid var(--ink);
  flex-shrink: 0;
}

.graph-fallback {
  padding: 48px;
  text-align: center;
  color: var(--rust);
  font-family: 'Fraunces', serif;
  font-style: italic;
  font-size: 1rem;
}

/* SVG node styles */
.gn-plugin { fill: var(--rust); stroke: var(--ink); stroke-width: 2; }
.gn-skill { fill: var(--blueprint); stroke: var(--ink); stroke-width: 1.5; }
.gn-script { fill: var(--amber); stroke: var(--ink); stroke-width: 1.5; }
.gn-event { fill: var(--sage); stroke: var(--ink); stroke-width: 2; }
.gn-state { fill: var(--rust); stroke: var(--ink); stroke-width: 2; }
.gn-perm { fill: var(--ink-fade); stroke: var(--ink); stroke-width: 1.5; }
.gn-rule { fill: var(--ink); stroke: var(--rust); stroke-width: 2; }

.gn-label {
  font-family: 'Fraunces', serif;
  font-size: 9px;
  font-weight: 500;
  fill: var(--ink);
  pointer-events: none;
  text-anchor: middle;
}

.gn-label-bg {
  fill: var(--paper);
  opacity: 0.92;
  pointer-events: none;
}

.gn-node { cursor: pointer; }
.gn-node:hover { filter: brightness(1.1); }

.dimmed { opacity: 0.15; }
.highlighted { opacity: 1; }

/* SVG link styles */
.gl { fill: none; pointer-events: stroke; }
.gl-contains { stroke: var(--blueprint); stroke-width: 1.5; }
.gl-triggers { stroke: var(--sage); stroke-width: 2; }
.gl-writes { stroke: var(--amber); stroke-width: 2; }
.gl-reads { stroke: var(--amber); stroke-width: 1.5; stroke-dasharray: 4 3; }
.gl-mandates { stroke: var(--rust); stroke-width: 2.5; }
.gl-references { stroke: var(--ink-fade); stroke-width: 1; stroke-dasharray: 2 3; }
.gl-blocks { stroke: var(--rust); stroke-width: 1.5; stroke-dasharray: 5 3; }
.gl-denies { stroke: var(--rust); stroke-width: 2; stroke-dasharray: 2 2; }

.gl-tooltip {
  position: absolute;
  background: var(--ink);
  color: var(--paper);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  padding: 4px 8px;
  pointer-events: none;
  z-index: 100;
  border: 1px solid var(--paper-shade);
}

@media (max-width: 900px) {
  .graph-container { grid-template-columns: 1fr; }
  .graph-svg-wrap { overflow-x: auto; }
  .graph-svg { min-width: 900px; }
  .graph-sidebar {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    border-top: 2px solid var(--ink);
    border-left: none; border-right: none; border-bottom: none;
    z-index: 50;
    max-height: 60vh;
    overflow-y: auto;
  }
  .graph-legend { grid-template-columns: 1fr; }
}

/* ============================================================
   PAGE LOAD ANIMATION
   ============================================================ */
@keyframes rise {
```

- [ ] **Step 3: Verify CSS добавился**

```bash
grep -c "graph-controls\|graph-svg\|gn-plugin\|gl-contains" \
  "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: число > 0 (минимум 4 уникальных селектора найдены).

- [ ] **Step 4: Verify D3 CDN tag добавлен**

```bash
grep -c "d3@7.9.0" "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: 1.

---

### Task 3: Insert new §X section HTML structure

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — insert section before §XI

- [ ] **Step 1: Insert new section перед §XI**

Use Edit tool. Найти:

```html
  <!-- ============================================================
       IX. ACTIONS
       ============================================================ -->
  <section class="section">
    <div class="frame">
      <div class="section-num display-i">XI</div>
      <h2 class="section-title">Что вы можете сделать</h2>
```

Заменить на:

```html
  <!-- ============================================================
       X. CONNECTIONS GRAPH
       ============================================================ -->
  <section class="section" id="graph-section">
    <div class="frame">
      <div class="section-num display-i">X</div>
      <h2 class="section-title">Связи &mdash; interactive map</h2>
      <p class="section-lede">
        50 узлов, 52 ребра, 8 типов связей. Кликни на любой узел &mdash;
        подсветятся все его связи + откроется панель с деталями справа.
        Тяни узлы мышкой для перестановки. Фильтры в верхней панели прячут
        категории по одной.
      </p>

      <div class="graph-controls">
        <div class="graph-filters">
          <button class="graph-filter active" data-type="plugin">Plugins</button>
          <button class="graph-filter active" data-type="skill">Skills</button>
          <button class="graph-filter active" data-type="script">Scripts</button>
          <button class="graph-filter active" data-type="event">Events</button>
          <button class="graph-filter active" data-type="state">State</button>
          <button class="graph-filter active" data-type="perm">Perms</button>
          <button class="graph-filter active" data-type="rule">Rules</button>
        </div>
        <button class="graph-reset">Reset layout</button>
      </div>

      <div class="graph-container">
        <div class="graph-svg-wrap">
          <svg class="graph-svg" viewBox="0 0 1100 700" preserveAspectRatio="xMidYMid meet"></svg>
        </div>
        <aside class="graph-sidebar">
          <div class="graph-sidebar-empty">
            Кликни на узел чтобы увидеть его связи&hellip;
          </div>
        </aside>
      </div>

      <div class="graph-legend">
        <div class="graph-legend-section">
          <h4>Узлы (категории)</h4>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--rust); border-radius:50%;"></span>Plugin (4)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--blueprint); border-radius:50%;"></span>Skill (28)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--amber); border-radius:50%;"></span>Hook script (7)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--sage);"></span>Hook event (5)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--rust); transform:rotate(45deg);"></span>State file (1)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--ink-fade);"></span>Permission (3)</div>
          <div class="graph-legend-item"><span class="graph-legend-swatch" style="background:var(--ink); border-color:var(--rust);"></span>Rule (2)</div>
        </div>
        <div class="graph-legend-section">
          <h4>Связи (типы)</h4>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--blueprint);"></span>contains</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--sage); border-width:2px;"></span>triggers</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--amber); border-width:2px;"></span>writes</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--amber); border-style:dashed;"></span>reads</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--rust); border-width:3px;"></span>mandates</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--ink-fade); border-style:dotted;"></span>references</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--rust); border-style:dashed;"></span>blocks</div>
          <div class="graph-legend-item"><span class="graph-legend-line" style="border-color:var(--rust); border-style:dashed; border-width:2px;"></span>denies</div>
        </div>
      </div>
    </div>
  </section>

  <!-- ============================================================
       XI. ACTIONS
       ============================================================ -->
  <section class="section">
    <div class="frame">
      <div class="section-num display-i">XI</div>
      <h2 class="section-title">Что вы можете сделать</h2>
```

- [ ] **Step 2: Verify HTML structure**

```bash
grep -c '<section' "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: 11 (было 10, добавили 1).

- [ ] **Step 3: Verify graph elements**

```bash
grep -cE 'class="graph-(svg|sidebar|filter|legend|reset)' "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: > 10.

---

### Task 4: Inline data — nodes (50)

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — append `<script>` block перед `</body>`

- [ ] **Step 1: Add nodes data script перед </body>**

Use Edit tool. Найти (последние строки файла):

```html
</main>

</body>
</html>
```

Заменить на:

```html
</main>

<script>
// ============================================================
// CONNECTIONS GRAPH DATA
// ============================================================
const GRAPH_NODES = [
  // === Plugins (4) ===
  { id: 'plg:superpowers', type: 'plugin', label: 'superpowers', desc: 'Главный плагин дисциплины процесса работы. 14 скилов.' },
  { id: 'plg:claude-md',   type: 'plugin', label: 'claude-md-management', desc: 'Единственный канал правок CLAUDE.md. 2 скила.' },
  { id: 'plg:fd',          type: 'plugin', label: 'frontend-design', desc: 'Создание distinctive frontend. Anthropic plugin.' },
  { id: 'plg:upm',         type: 'plugin', label: 'ui-ux-pro-max', desc: 'Резерв-библиотека UI/UX (50+ стилей, 161 палитра).' },

  // === Skills (28) ===
  // Superpowers (14)
  { id: 'skl:brainstorming', type: 'skill', label: 'brainstorming', desc: 'Превращает идею в спек через диалог.' },
  { id: 'skl:writing-plans', type: 'skill', label: 'writing-plans', desc: 'Из спека → пошаговый план.' },
  { id: 'skl:executing-plans', type: 'skill', label: 'executing-plans', desc: 'Исполняет план по шагам.' },
  { id: 'skl:tdd', type: 'skill', label: 'test-driven-development', desc: 'Тест ДО кода. Red-green-refactor.' },
  { id: 'skl:debug', type: 'skill', label: 'systematic-debugging', desc: '4 фазы root cause. ≥3 гипотезы.' },
  { id: 'skl:req-review', type: 'skill', label: 'requesting-code-review', desc: 'Перед merge — двухстадийный review.' },
  { id: 'skl:recv-review', type: 'skill', label: 'receiving-code-review', desc: 'Обработка feedback ревью.' },
  { id: 'skl:verify', type: 'skill', label: 'verification-before-completion', desc: 'Перед claim готово — verify.' },
  { id: 'skl:finishing', type: 'skill', label: 'finishing-a-development-branch', desc: 'merge / PR / cleanup.' },
  { id: 'skl:worktrees', type: 'skill', label: 'using-git-worktrees', desc: 'Изоляция feature-работы.' },
  { id: 'skl:subagent', type: 'skill', label: 'subagent-driven-development', desc: 'Свежий subagent на каждую задачу.' },
  { id: 'skl:parallel', type: 'skill', label: 'dispatching-parallel-agents', desc: 'Параллельные subagent\'ы.' },
  { id: 'skl:using-sp', type: 'skill', label: 'using-superpowers', desc: 'Базовый: как находить skills.' },
  { id: 'skl:writing-skills', type: 'skill', label: 'writing-skills', desc: 'Создание новых skills.' },
  // claude-md-management (2)
  { id: 'skl:md-improver', type: 'skill', label: 'claude-md-improver', desc: 'Audit + targeted updates CLAUDE.md.' },
  { id: 'skl:md-revise', type: 'skill', label: 'revise-claude-md', desc: 'Захват session-learnings.' },
  // frontend-design (1)
  { id: 'skl:fd-skill', type: 'skill', label: 'frontend-design', desc: 'Distinctive UI без AI-aesthetics.' },
  // ui-ux-pro-max (1)
  { id: 'skl:upm-skill', type: 'skill', label: 'ui-ux-pro-max', desc: 'Резерв-библиотека стилей.' },
  // Standalone (10)
  { id: 'skl:update-config', type: 'skill', label: 'update-config', desc: 'Правки settings.json.' },
  { id: 'skl:keybindings', type: 'skill', label: 'keybindings-help', desc: 'Клавиатурные сокращения.' },
  { id: 'skl:simplify', type: 'skill', label: 'simplify', desc: 'Review кода на reuse/quality.' },
  { id: 'skl:fewer-prompts', type: 'skill', label: 'fewer-permission-prompts', desc: 'Снижение шума prompts.' },
  { id: 'skl:init', type: 'skill', label: 'init', desc: 'Новый CLAUDE.md для нового проекта.' },
  { id: 'skl:claude-api', type: 'skill', label: 'claude-api', desc: 'Claude API / SDK apps.' },
  { id: 'skl:loop', type: 'skill', label: 'loop', desc: 'Recurring prompt на интервале.' },
  { id: 'skl:schedule', type: 'skill', label: 'schedule', desc: 'Cron-расписания для агентов.' },
  { id: 'skl:review', type: 'skill', label: 'review', desc: 'Review текущего PR.' },
  { id: 'skl:sec-review', type: 'skill', label: 'security-review', desc: 'Security audit pending changes.' },

  // === Hook scripts (7) ===
  { id: 'scr:skill-marker', type: 'script', label: 'skill-marker.py', desc: 'Отметка о вызове Skill.' },
  { id: 'scr:skill-check', type: 'script', label: 'skill-check.py', desc: 'Reminder §12 если skill не вызван.' },
  { id: 'scr:economy-mode', type: 'script', label: 'economy-mode.py', desc: 'Парсит экономию N%, пишет state.' },
  { id: 'scr:economy-self-check', type: 'script', label: 'economy-self-check.py', desc: 'SessionStart runtime guard.' },
  { id: 'scr:economy-state-guard', type: 'script', label: 'economy-state-guard.py', desc: 'PreToolUse reminder + Bash bypass.' },
  { id: 'scr:economy-verifier', type: 'script', label: 'economy-verifier.py (Sonnet 4.6)', desc: 'Stop verifier — блокирует cherry-pick.' },
  { id: 'scr:economy-postcompact', type: 'script', label: 'economy-postcompact.py', desc: 'Re-inject правил после компакции.' },

  // === Hook events (5) ===
  { id: 'evt:session-start', type: 'event', label: 'SessionStart', desc: 'Один раз на старте сессии.' },
  { id: 'evt:user-prompt-submit', type: 'event', label: 'UserPromptSubmit', desc: 'Каждый submit от пользователя.' },
  { id: 'evt:pre-tool-use', type: 'event', label: 'PreToolUse', desc: 'Перед каждым tool call.' },
  { id: 'evt:post-compact', type: 'event', label: 'PostCompact', desc: 'После авто-компакции.' },
  { id: 'evt:stop', type: 'event', label: 'Stop', desc: 'Конец моего ответа.' },

  // === State file (1) ===
  { id: 'st:economy-state', type: 'state', label: 'claude-economy-state.json', desc: '$TEMP/claude-economy-<session_id>.json — shared state.' },

  // === Permissions (3) ===
  { id: 'prm:allow', type: 'perm', label: 'permissions.allow (1)', desc: 'Разрешено без вопросов: Bash(git push origin main:*).' },
  { id: 'prm:deny', type: 'perm', label: 'permissions.deny (7)', desc: 'Жёстко заблокировано: rm/mv hook/settings/state.' },
  { id: 'prm:ask', type: 'perm', label: 'permissions.ask (16)', desc: 'С approve пользователя: Edit/Write hook files и settings.json.' },

  // === Rules (2) ===
  { id: 'rul:pravila-12', type: 'rule', label: 'Pravila §12', desc: 'Hard rule: Superpowers skill ПЕРВЫМ.' },
  { id: 'rul:claude-md', type: 'rule', label: 'CLAUDE.md', desc: 'Главная карта проекта.' }
];

console.log('GRAPH_NODES:', GRAPH_NODES.length, '(expected 50)');
</script>

</body>
</html>
```

- [ ] **Step 2: Verify nodes count**

Open the file в браузере, посмотри Console (F12 → Console tab). Expected log: `GRAPH_NODES: 50 (expected 50)`.

Или offline:

```bash
grep -c "^  { id:" "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: 50.

---

### Task 5: Inline data — links (52)

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить GRAPH_LINKS после GRAPH_NODES

- [ ] **Step 1: Add links data**

Use Edit tool. Найти:

```js
console.log('GRAPH_NODES:', GRAPH_NODES.length, '(expected 50)');
</script>
```

Заменить на:

```js
console.log('GRAPH_NODES:', GRAPH_NODES.length, '(expected 50)');

const GRAPH_LINKS = [
  // === contains (18) — plugin → skill ===
  // superpowers (14)
  { source: 'plg:superpowers', target: 'skl:brainstorming', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:writing-plans', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:executing-plans', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:tdd', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:debug', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:req-review', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:recv-review', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:verify', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:finishing', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:worktrees', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:subagent', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:parallel', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:using-sp', type: 'contains' },
  { source: 'plg:superpowers', target: 'skl:writing-skills', type: 'contains' },
  // claude-md (2)
  { source: 'plg:claude-md', target: 'skl:md-improver', type: 'contains' },
  { source: 'plg:claude-md', target: 'skl:md-revise', type: 'contains' },
  // fd (1)
  { source: 'plg:fd', target: 'skl:fd-skill', type: 'contains' },
  // upm (1)
  { source: 'plg:upm', target: 'skl:upm-skill', type: 'contains' },

  // === triggers (7) — event → script ===
  { source: 'evt:session-start', target: 'scr:economy-self-check', type: 'triggers' },
  { source: 'evt:user-prompt-submit', target: 'scr:economy-mode', type: 'triggers' },
  { source: 'evt:pre-tool-use', target: 'scr:skill-marker', type: 'triggers' },
  { source: 'evt:pre-tool-use', target: 'scr:skill-check', type: 'triggers' },
  { source: 'evt:pre-tool-use', target: 'scr:economy-state-guard', type: 'triggers' },
  { source: 'evt:post-compact', target: 'scr:economy-postcompact', type: 'triggers' },
  { source: 'evt:stop', target: 'scr:economy-verifier', type: 'triggers' },

  // === writes (1) — script → state ===
  { source: 'scr:economy-mode', target: 'st:economy-state', type: 'writes' },

  // === reads (3) — script ← state (модель: source reads target) ===
  { source: 'scr:economy-state-guard', target: 'st:economy-state', type: 'reads' },
  { source: 'scr:economy-verifier', target: 'st:economy-state', type: 'reads' },
  { source: 'scr:economy-postcompact', target: 'st:economy-state', type: 'reads' },

  // === mandates (14) — Pravila §12 → 14 superpowers skills ===
  { source: 'rul:pravila-12', target: 'skl:brainstorming', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:writing-plans', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:executing-plans', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:tdd', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:debug', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:req-review', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:recv-review', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:verify', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:finishing', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:worktrees', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:subagent', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:parallel', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:using-sp', type: 'mandates' },
  { source: 'rul:pravila-12', target: 'skl:writing-skills', type: 'mandates' },

  // === references (1) — CLAUDE.md → Pravila §12 ===
  { source: 'rul:claude-md', target: 'rul:pravila-12', type: 'references' },

  // === blocks (7) — permissions.ask → hook scripts ===
  { source: 'prm:ask', target: 'scr:skill-marker', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:skill-check', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:economy-mode', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:economy-self-check', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:economy-state-guard', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:economy-verifier', type: 'blocks' },
  { source: 'prm:ask', target: 'scr:economy-postcompact', type: 'blocks' },

  // === denies (1) — permissions.deny → state file ===
  { source: 'prm:deny', target: 'st:economy-state', type: 'denies' }
];

console.log('GRAPH_LINKS:', GRAPH_LINKS.length, '(expected 52)');
</script>
```

- [ ] **Step 2: Verify в браузере**

Open в Chrome (или другом), F12 → Console. Expected log:

```
GRAPH_NODES: 50 (expected 50)
GRAPH_LINKS: 52 (expected 52)
```

---

### Task 6: D3 force simulation + initial rendering

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить simulation код в существующий `<script>` блок

- [ ] **Step 1: Add D3 simulation и render code**

Use Edit tool. Найти:

```js
console.log('GRAPH_LINKS:', GRAPH_LINKS.length, '(expected 52)');
</script>
```

Заменить на:

```js
console.log('GRAPH_LINKS:', GRAPH_LINKS.length, '(expected 52)');

// ============================================================
// D3 SIMULATION + RENDER
// ============================================================
window.addEventListener('DOMContentLoaded', () => {
  if (typeof d3 === 'undefined') {
    document.querySelector('.graph-svg-wrap').innerHTML =
      '<div class="graph-fallback">⚠ D3.js не загружен (нужен интернет для CDN). Остальная страница работает offline.</div>';
    return;
  }

  const W = 1100, H = 700;
  const svg = d3.select('.graph-svg');

  // Y-position по типу для clustering
  const Y_BY_TYPE = {
    'rule': 80,
    'plugin': 200,
    'skill': 340,
    'event': 540,
    'script': 460,
    'state': 380,
    'perm': 600
  };

  // Distance по типу связи
  const LINK_DISTANCE = {
    'contains': 60,
    'triggers': 50,
    'writes': 40,
    'reads': 40,
    'mandates': 100,
    'references': 80,
    'blocks': 70,
    'denies': 50
  };

  // Size по типу узла
  const NODE_SIZE = {
    'plugin': 22, 'skill': 10, 'script': 14, 'event': 14,
    'state': 16, 'perm': 16, 'rule': 18
  };

  // Build simulation
  const sim = d3.forceSimulation(GRAPH_NODES)
    .force('link', d3.forceLink(GRAPH_LINKS).id(d => d.id)
      .distance(d => LINK_DISTANCE[d.type] || 50)
      .strength(0.5))
    .force('charge', d3.forceManyBody().strength(-280))
    .force('center', d3.forceCenter(W / 2, H / 2))
    .force('y', d3.forceY(d => Y_BY_TYPE[d.type] || H/2).strength(0.18))
    .force('collide', d3.forceCollide().radius(d => (NODE_SIZE[d.type] || 10) + 6));

  // Render links
  const linkSel = svg.append('g').attr('class', 'links')
    .selectAll('line')
    .data(GRAPH_LINKS)
    .join('line')
    .attr('class', d => `gl gl-${d.type}`);

  // Render nodes как <g>
  const nodeSel = svg.append('g').attr('class', 'nodes')
    .selectAll('g.gn-node')
    .data(GRAPH_NODES)
    .join('g')
    .attr('class', d => `gn-node gn-node-${d.type}`);

  // Shape по type: circle / rect / diamond / hexagon
  nodeSel.each(function(d) {
    const g = d3.select(this);
    const size = NODE_SIZE[d.type];
    if (d.type === 'plugin' || d.type === 'skill' || d.type === 'script') {
      g.append('circle').attr('r', size).attr('class', `gn-${d.type}`);
    } else if (d.type === 'event') {
      g.append('rect').attr('x', -size).attr('y', -size)
        .attr('width', size*2).attr('height', size*2).attr('class', 'gn-event');
    } else if (d.type === 'state') {
      g.append('polygon')
        .attr('points', `0,-${size} ${size},0 0,${size} -${size},0`)
        .attr('class', 'gn-state');
    } else if (d.type === 'perm') {
      const s = size;
      g.append('polygon')
        .attr('points', `-${s},-${s/2} 0,-${s} ${s},-${s/2} ${s},${s/2} 0,${s} -${s},${s/2}`)
        .attr('class', 'gn-perm');
    } else if (d.type === 'rule') {
      g.append('rect').attr('x', -size).attr('y', -size)
        .attr('width', size*2).attr('height', size*2).attr('class', 'gn-rule');
    }
  });

  // Labels — text с background rect
  nodeSel.append('text')
    .attr('class', 'gn-label')
    .attr('dy', d => (NODE_SIZE[d.type] || 10) + 14)
    .text(d => d.label);

  // Tick — обновление позиций
  sim.on('tick', () => {
    linkSel
      .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
      .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
    nodeSel.attr('transform', d => `translate(${d.x},${d.y})`);
  });
});
</script>
```

- [ ] **Step 2: Open в браузере и verify**

Open file. Should see граф с узлами разных типов, цветов, форм. Labels под узлами. Линии соединяют связанные узлы. Через 2-3 секунды simulation стабилизируется.

В Console F12: видны логи NODES/LINKS. Никаких ошибок.

Expected (визуально):

- 50 узлов рендерятся (можно посчитать через `document.querySelectorAll('.gn-node').length` в Console)
- 52 ребра рендерятся (`document.querySelectorAll('.gl').length`)
- Узлы группируются по Y-категории (rules сверху, plugins выше, perms внизу)

---

### Task 7: Drag behavior

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить drag behavior после tick

- [ ] **Step 1: Add drag**

Use Edit tool. Найти:

```js
  // Tick — обновление позиций
  sim.on('tick', () => {
```

Заменить на:

```js
  // Drag behavior
  function dragstart(event, d) {
    if (!event.active) sim.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }
  function dragmove(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }
  function dragend(event, d) {
    if (!event.active) sim.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }
  nodeSel.call(d3.drag()
    .on('start', dragstart)
    .on('drag', dragmove)
    .on('end', dragend));

  // Tick — обновление позиций
  sim.on('tick', () => {
```

- [ ] **Step 2: Verify drag в браузере**

Открой файл, попробуй перетащить любой узел мышкой. Узел должен следовать за курсором, остальные узлы реагируют forces.

После отпускания — узел возвращается под действие forces, не фиксируется.

---

### Task 8: Click → highlight + sidebar populate

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить click handler перед drag setup

- [ ] **Step 1: Add click highlight + sidebar populate**

Use Edit tool. Найти:

```js
  // Drag behavior
  function dragstart(event, d) {
```

Заменить на:

```js
  // Click → highlight + sidebar
  const sidebar = document.querySelector('.graph-sidebar');
  let activeId = null;

  function showSidebar(node) {
    const outgoing = GRAPH_LINKS.filter(l =>
      (typeof l.source === 'object' ? l.source.id : l.source) === node.id);
    const incoming = GRAPH_LINKS.filter(l =>
      (typeof l.target === 'object' ? l.target.id : l.target) === node.id);

    const out = outgoing.map(l => {
      const tgtId = typeof l.target === 'object' ? l.target.id : l.target;
      const tgt = GRAPH_NODES.find(n => n.id === tgtId);
      return `<li><b>${l.type}</b> → ${tgt ? tgt.label : tgtId}</li>`;
    }).join('');
    const inc = incoming.map(l => {
      const srcId = typeof l.source === 'object' ? l.source.id : l.source;
      const src = GRAPH_NODES.find(n => n.id === srcId);
      return `<li>${src ? src.label : srcId} <b>← ${l.type}</b></li>`;
    }).join('');

    sidebar.innerHTML = `
      <button class="graph-close" aria-label="Close">×</button>
      <div class="graph-sidebar-title">${node.label}</div>
      <div class="graph-sidebar-badge">${node.type}</div>
      <div style="font-size:0.85rem; color: var(--ink-soft); line-height: 1.5;">${node.desc}</div>
      ${outgoing.length ? `
      <div class="graph-sidebar-section">
        <div class="graph-sidebar-section-label">Исходящие (${outgoing.length})</div>
        <ul class="graph-conn-list">${out}</ul>
      </div>` : ''}
      ${incoming.length ? `
      <div class="graph-sidebar-section">
        <div class="graph-sidebar-section-label">Входящие (${incoming.length})</div>
        <ul class="graph-conn-list">${inc}</ul>
      </div>` : ''}
    `;
    sidebar.querySelector('.graph-close').addEventListener('click', clearHighlight);
  }

  function highlightNode(node) {
    activeId = node.id;
    const relatedIds = new Set([node.id]);
    GRAPH_LINKS.forEach(l => {
      const sId = typeof l.source === 'object' ? l.source.id : l.source;
      const tId = typeof l.target === 'object' ? l.target.id : l.target;
      if (sId === node.id) relatedIds.add(tId);
      if (tId === node.id) relatedIds.add(sId);
    });

    nodeSel.classed('dimmed', d => !relatedIds.has(d.id));
    linkSel.classed('dimmed', l => {
      const sId = typeof l.source === 'object' ? l.source.id : l.source;
      const tId = typeof l.target === 'object' ? l.target.id : l.target;
      return sId !== node.id && tId !== node.id;
    });
    showSidebar(node);
  }

  function clearHighlight() {
    activeId = null;
    nodeSel.classed('dimmed', false);
    linkSel.classed('dimmed', false);
    sidebar.innerHTML = '<div class="graph-sidebar-empty">Кликни на узел чтобы увидеть его связи…</div>';
  }

  nodeSel.on('click', (event, d) => {
    event.stopPropagation();
    if (activeId === d.id) clearHighlight();
    else highlightNode(d);
  });

  svg.on('click', clearHighlight);

  // Drag behavior
  function dragstart(event, d) {
```

- [ ] **Step 2: Verify click highlight в браузере**

Refresh страницу. Кликни на любой plugin узел (e.g., superpowers). Expected:

- Plugin узел остаётся ярким
- Все его связанные skills (14 для superpowers) — ярки
- Остальные узлы и линии — затемнены до opacity 0.15
- Sidebar справа показывает: имя плагина, описание, секцию «Исходящие» с 14 пунктами вида `contains → brainstorming` etc.

Click на пустое место SVG → подсветка сбрасывается, sidebar возвращается в пустое состояние.

---

### Task 9: Hover edge tooltip

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить tooltip элемент + handlers

- [ ] **Step 1: Add tooltip**

Use Edit tool. Найти:

```js
  svg.on('click', clearHighlight);
```

Заменить на:

```js
  svg.on('click', clearHighlight);

  // Hover edge → tooltip
  const tooltip = d3.select(document.body)
    .append('div').attr('class', 'gl-tooltip').style('display', 'none');

  linkSel.on('mouseover', function(event, l) {
    const sId = typeof l.source === 'object' ? l.source.id : l.source;
    const tId = typeof l.target === 'object' ? l.target.id : l.target;
    const src = GRAPH_NODES.find(n => n.id === sId);
    const tgt = GRAPH_NODES.find(n => n.id === tId);
    tooltip.html(`<b>${l.type}</b>: ${src.label} → ${tgt.label}`)
      .style('display', 'block')
      .style('left', (event.pageX + 12) + 'px')
      .style('top', (event.pageY - 8) + 'px');
    d3.select(this).style('stroke-width', '4');
  });
  linkSel.on('mousemove', function(event) {
    tooltip.style('left', (event.pageX + 12) + 'px').style('top', (event.pageY - 8) + 'px');
  });
  linkSel.on('mouseout', function() {
    tooltip.style('display', 'none');
    d3.select(this).style('stroke-width', null);
  });
```

- [ ] **Step 2: Verify hover tooltip**

Refresh. Наведи курсор на любую линию между узлами. Expected:

- Появляется чёрный tooltip с типом связи и source → target
- Линия становится толще (4px)
- При отведении курсора — tooltip исчезает, линия возвращается к нормальной толщине

---

### Task 10: Filter chips + reset button

**Files:**

- Modify: `docs/visualizations/hooks-skills-plugins-map.html` — добавить filter и reset handlers

- [ ] **Step 1: Add filter и reset**

Use Edit tool. Найти:

```js
  linkSel.on('mouseout', function() {
    tooltip.style('display', 'none');
    d3.select(this).style('stroke-width', null);
  });
```

Заменить на:

```js
  linkSel.on('mouseout', function() {
    tooltip.style('display', 'none');
    d3.select(this).style('stroke-width', null);
  });

  // Filter chips
  const filters = document.querySelectorAll('.graph-filter');
  const hiddenTypes = new Set();

  function applyFilters() {
    nodeSel.style('display', d => hiddenTypes.has(d.type) ? 'none' : null);
    linkSel.style('display', l => {
      const sT = (typeof l.source === 'object' ? l.source.type : GRAPH_NODES.find(n=>n.id===l.source).type);
      const tT = (typeof l.target === 'object' ? l.target.type : GRAPH_NODES.find(n=>n.id===l.target).type);
      return (hiddenTypes.has(sT) || hiddenTypes.has(tT)) ? 'none' : null;
    });
  }

  filters.forEach(btn => {
    btn.addEventListener('click', () => {
      const type = btn.dataset.type;
      if (hiddenTypes.has(type)) {
        hiddenTypes.delete(type);
        btn.classList.add('active');
      } else {
        hiddenTypes.add(type);
        btn.classList.remove('active');
      }
      applyFilters();
    });
  });

  // Reset button
  document.querySelector('.graph-reset').addEventListener('click', () => {
    hiddenTypes.clear();
    filters.forEach(b => b.classList.add('active'));
    applyFilters();
    clearHighlight();
    GRAPH_NODES.forEach(n => { n.fx = null; n.fy = null; });
    sim.alpha(0.7).restart();
  });
```

- [ ] **Step 2: Verify filters и reset**

Refresh. Кликни на «Plugins» chip (он станет неактивным — без чёрного фона). Expected:

- 4 plugin узла исчезают
- 18 contains-edges (их incident) тоже исчезают
- Skills остаются как «standalone» узлы без parent

Кликни «Plugins» снова — вернутся.

Кликни «Reset layout» — все filters снова active, simulation перезапускается, узлы reorganize.

---

### Task 11: Final verification (success criteria)

**Files:**

- Open: `docs/visualizations/hooks-skills-plugins-map.html` в браузере

- [ ] **Step 1: Visual checklist в браузере**

Открой файл (двойной клик в Проводнике или `cmd //c start "" "..."`). Пройди checklist:

- [ ] Page загружается без ошибок (F12 → Console, 0 errors)
- [ ] Console log: `GRAPH_NODES: 50 (expected 50)` и `GRAPH_LINKS: 52 (expected 52)`
- [ ] §X «Связи — interactive map» видна между §IX (Mental Model) и §XI (Actions)
- [ ] 50 узлов рендерятся (граф наполнен)
- [ ] 52 линии между узлами видны
- [ ] Узлы кластеризованы по типу (rules сверху, plugins выше, events внизу)
- [ ] Drag узла работает: можно перетащить, остальные реагируют
- [ ] Click на plugin узел → подсветка skills + sidebar с 14 пунктами «contains →»
- [ ] Click на state-file → подсветка 1 writer + 3 readers + 1 denier (5 connections)
- [ ] Click на Pravila §12 → подсветка 14 superpowers skills + reference на CLAUDE.md
- [ ] Hover на линии → tooltip с типом связи
- [ ] Filter «Skills» off → 28 skill узлов исчезают
- [ ] Filter «Skills» on → возвращаются
- [ ] Reset → все filters active, sim restarts
- [ ] Mobile width <900px (DevTools → Toggle device toolbar): граф horizontally scrolls, sidebar становится bottom-sheet

- [ ] **Step 2: Verify HTML structure**

```bash
PYTHONIOENCODING=utf-8 python -c "
from html.parser import HTMLParser
class P(HTMLParser):
    def __init__(self):
        super().__init__()
        self.opened = []
        self.unmatched = 0
    def handle_starttag(self, tag, attrs):
        if tag not in ('br','img','input','meta','link','hr','source'):
            self.opened.append(tag)
    def handle_endtag(self, tag):
        if self.opened and self.opened[-1] == tag:
            self.opened.pop()
        else:
            self.unmatched += 1
with open(r'c:\моя\проекты\портал crm\Документация\docs\visualizations\hooks-skills-plugins-map.html', encoding='utf-8') as f:
    p = P(); p.feed(f.read())
print('unclosed:', len(p.opened), 'orphans:', p.unmatched)
"
```

Expected: `unclosed: 0 orphans: 0`.

- [ ] **Step 3: Verify file size**

```bash
wc -lc "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"
```

Expected: ~2500 строк (было 2240, +260).

---

## Self-Review

### Spec coverage

| Spec section | Plan task |
|---|---|
| §1 Goal (interactive force-directed) | Task 6 (D3 simulation), Task 8 (click), Task 7 (drag) |
| §2.1 Scope: D3, ~50 nodes | Tasks 4-6 |
| §2.2 Out of scope (search, export, zoom) | НЕ имплементируется (verified в Task 11) |
| §3.1 Tech: D3 v7 CDN | Task 2 step 1 |
| §3.2 Data: 50 nodes / 52 edges | Tasks 4-5 |
| §3.3 Visual encoding (shapes/colors) | Task 2 step 2 (CSS), Task 6 step 1 (shape rendering) |
| §3.4 Initial positioning | Task 6 step 1 (`Y_BY_TYPE` clustering) |
| §3.5 Interactivity (drag/click/hover/filter/reset) | Tasks 7-10 |
| §3.6 Sidebar | Task 8 step 1 (showSidebar) |
| §3.7 Category filters | Task 10 step 1 |
| §4.1 HTML structure | Task 3 step 1 |
| §4.2 JS module | Tasks 4-10 (inline scripts) |
| §4.3 CSS additions | Task 2 step 2 |
| §5.1 Manual verification checklist | Task 11 step 1 |
| §5.2 Browser compat | Не имплементируется (Chrome 120+ assumed) |
| §6 Risks: CDN fallback | Task 6 step 1 (`typeof d3 === 'undefined'`) |
| §7 Success criteria | Task 11 |
| §8 Cascading (renumber §X → §XI) | Task 1 |

✅ Все spec sections покрыты.

### Placeholder scan

- ✅ Никаких TBD / TODO / `Similar to Task N`
- ✅ Полный код в каждом step
- ✅ Exact commands с expected output
- ✅ Exact file paths

### Type consistency

- ✅ `GRAPH_NODES` массив — везде с тем же именем
- ✅ `GRAPH_LINKS` массив — везде с тем же именем
- ✅ Field names согласованы: `id`, `type`, `label`, `desc` для nodes; `source`, `target`, `type` для links
- ✅ CSS class names: `gn-{type}` для node shapes, `gl-{type}` для link styles — consistent
- ✅ Function names: `showSidebar`, `highlightNode`, `clearHighlight`, `applyFilters`, `dragstart/dragmove/dragend` — без drift'ов

---

## Execution Handoff

Plan complete and saved to `docs/superpowers/plans/2026-05-10-connections-graph.md` (~750 строк).

**Two execution options:**

**1. Subagent-Driven (recommended)** — диспатчу свежего subagent'a на каждую task, review между ними. Изоляция контекста. Однако: всё работает с одним HTML файлом — нет параллелизма, последовательные tasks. ~11 dispatches.

**2. Inline Execution** — выполняю tasks в этой сессии через executing-plans skill. Batch с checkpoints. Учитывая, что я уже глубоко в контексте файла (только что писал spec и иерархию), inline может быть эффективнее.

**Какой подход выбираешь?**
