Files
portal/docs/superpowers/plans/2026-05-10-connections-graph.md
T
Дмитрий 5e38ff6d7e docs(viz): connections-graph spec + plan (D3 force-directed карта связей)
Артефакты параллельной Claude-сессии (mode «экономия 0%», 10.05.2026 ночь).

Spec (385 строк): расширение hooks-skills-plugins-map.html новой §X
«Связи — interactive map». Force-directed network через D3.js v7 (CDN),
~50 узлов (плагины + скилы + хук-скрипты + hook events + state-файл +
permissions + Pravila §12 + CLAUDE.md), 52 ребра. Vintage-blueprint
aesthetic. Drag/click/hover/category filters/reset.

Plan (1246 строк): пошаговая реализация для executing-plans с TDD-структурой.

cspell-words.txt: +3 термина (диспатчу/скилы/ребёр).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:31:44 +03:00

48 KiB
Raw Blame History

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 (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. Найти:

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

Заменить на:

      <div class="section-num display-i">XI</div>
      <h2 class="section-title">Что вы можете сделать</h2>
  • Step 2: Verify единственный X→XI выполнен
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. Найти:

<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>

Заменить на:

<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. Найти:

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

Заменить на:

/* ============================================================
   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 добавился
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 добавлен
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. Найти:

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

Заменить на:

  <!-- ============================================================
       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
grep -c '<section' "c:/моя/проекты/портал crm/Документация/docs/visualizations/hooks-skills-plugins-map.html"

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

  • Step 3: Verify graph elements
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 перед

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

</main>

</body>
</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:

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

Expected: 50.


Files:

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

  • Step 1: Add links data

Use Edit tool. Найти:

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

Заменить на:

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. Найти:

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

Заменить на:

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. Найти:

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

Заменить на:

  // 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. Найти:

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

Заменить на:

  // 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. Найти:

  svg.on('click', clearHighlight);

Заменить на:

  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. Найти:

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

Заменить на:

  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

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
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 может быть эффективнее.

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