# Карта автоматизации — iter6: даты, счётчик использования, дубли узлов

**Дата:** 2026-05-16
**Артефакт:** `docs/automation-graph.html` (vis.js Network 9.1.9, single-file HTML)
**Тип:** дизайн-спецификация (brainstorming → этот документ → writing-plans)
**Предыдущая итерация:** iter5 — фактический реколлаж блока ruflo (`docs/superpowers/specs/2026-05-15-automation-graph-iter5-ruflo-factual-design.md`)

---

## 1. Контекст и цель

Заказчик (Дмитрий) запросил три добавления к карте:

1. В легенду каждого узла — раздел с **датой внедрения** и **датой последнего изменения**.
2. В легенду узла — **количество вызовов узла за последние 7 дней** + кнопка подсветки.
3. **Анализ узлов, дублирующих друг друга на ≥80%** + кнопка подсветки дублей внизу легенды.

Сквозное требование (как в iter5): данные **фактические**, из рантайма и истории, а не из нормативной документации. Счётчик использования при отсутствии настоящей телеметрии нельзя выдумывать — он берётся из реальных логов либо честно помечается «нет данных».

**Терминология карты.** «Легенда узла» = правая панель `#legend-node-content` (строки 94–107, рендер `showNodeLegend` строки 1386–1440), 8 секций. «Низ легенды» = нижняя панель `#cat-legend` (строки 123–137) — 13 кликабельных бейджей-фильтров категорий.

---

## 2. Подход

**Выбран Вариант A — статичные данные, вшитые в HTML.** Новые блоки-константы в `<script>`; значения генерируются на этапе реализации из git, changelog/memory и разбора транскриптов сессий, затем вшиваются в файл. Рендер легенды и кнопки читают эти константы.

Отклонённые варианты (для протокола):

- **B — sidecar `*.json` + `fetch()`:** карта открывается как локальный файл (`file://`), браузеры блокируют `fetch()` локальных файлов (CORS) — карта перестанет работать при открытии двойным кликом. Это зона квирка #90.
- **C — живой подсчёт в браузере:** невозможно — у браузера нет доступа к транскриптам сессий и git.

Снимок-природа данных раскрывается явно: легенда пишет «срез 09–16.05.2026». Карта **уже** ручной артефакт — поля `when`/`limits` правятся вручную каждую итерацию; новые поля встают в тот же режим.

---

## 3. Модель данных

Две новые константы + два константа-параметра. Размещение — новая `SECTION 3.6` между `SECTION 3.5` (EDGE_DETAILS, заканчивается строкой 1328) и `SECTION 4` (VIS INIT, строка 1330).

### 3.1. Параметры снимка

```js
const META_SNAPSHOT = '16.05.2026';        // дата генерации значений
const META_WINDOW   = '09–16.05.2026';     // окно подсчёта использования (7 дней)
```

### 3.2. `NODE_META` — метаданные по каждому узлу

Ключ = id узла. Покрытие — все 83 узла (включая `mem_ruflo`).

```js
const NODE_META = {
  pravila: { since: '…', changed: '…', uses: null, usesSrc: '—' },
  // … 83 записи
};
```

| поле | тип | смысл |
|---|---|---|
| `since` | string | дата внедрения `'DD.MM.YYYY'`, либо `'—'`, либо короткая фраза (`'встроено в Claude Code'`) |
| `changed` | string | дата последнего изменения `'DD.MM.YYYY'` либо `'—'` |
| `uses` | number \| null | вызовов за 7 дней. `0` = измеримый узел, реально простаивал. `null` = измерить нельзя (узел-правило) → в легенде «нет данных», **не** «0» |
| `usesSrc` | string | короткая метка источника числа: `'скил'` / `'агент'` / `'MCP'` / `'хук'` / `'memory-чтение'` / `'коммиты'` / `'инспекция'` / `'—'` |
| `dupNote` | string (опц.) | только для 3 ruflo-узлов из ролевой избыточности (R-группы §6.2); короткая заметка о дублировании роли |

Поле `dupNote` присутствует только там, где нужно (необязательное). Парные дубли (D-группы) в `NODE_META` не дублируются — они выводятся через `DUPLICATE_GROUPS`.

### 3.3. `DUPLICATE_GROUPS` — явные парные дубли

```js
const DUPLICATE_GROUPS = [
  { id: 'D1', pct: 90, members: ['hk_post_md', 'lh_mdlint'],
    reason: 'оба запускают markdownlint --fix на .md — разница только в триггере (после правки в сессии против при коммите)' },
  { id: 'D2', pct: 85, members: ['lh_gitleaks', 'lh_gitleaks2'],
    reason: 'оба — скан секретов gitleaks; pre-commit проверяет staged-файлы, pre-push — всю историю (надмножество)' },
  { id: 'D3', pct: 85, members: ['sk_rls', 'ag_rls'],
    reason: 'оба — 7-пунктовая проверка RLS-соответствия; уже отмечено 🔴-конфликтом «нет регламента кто когда»' },
  { id: 'D4', pct: 80, members: ['sk_eplans', 'sk_subagent'],
    reason: 'оба исполняют план writing-plans пошагово; разница — параллельная сессия против суб-агентов в текущей' },
  { id: 'D5', pct: 80, members: ['sk_wplans', 'ag_plan'],
    reason: 'оба производят план задачи; скил writing-plans — inline с полным кодом, агент Plan — архитектурный набросок READ-ONLY' },
  { id: 'D7', pct: 80, members: ['ruflo_recall_hook', 'hk_session'],
    reason: 'оба инжектят память в контекст Клода; recall-хук — на каждый промпт из базы ruflo, SessionStart — при старте из .md-файлов' },
];
```

Нумерация (`D1`–`D5`, `D7`) сохранена из обсуждения с заказчиком: `D6` в обсуждении отнесён к ролевой избыточности ruflo (§6.2), поэтому в массиве парных дублей его нет — это намеренный пропуск, а не ошибка.

Производный индекс для рендера и подсветки строится один раз при инициализации:

```js
const DUP_BY_NODE = new Map(); // nodeId -> { partner, pct, reason }
DUPLICATE_GROUPS.forEach(g => g.members.forEach(m => {
  const partner = g.members.find(x => x !== m);
  DUP_BY_NODE.set(m, { partner, pct: g.pct, reason: g.reason });
}));
const DUP_NODE_SET = new Set(DUP_BY_NODE.keys()); // все узлы-члены парных дублей
```

### 3.4. Что НЕ меняется (scope-guard)

Чисто аддитивная итерация. Не трогаются: `NODES` (83), `EDGES` (90), `CONFLICT_TYPES`, builder `nd()`, все 83 записи `NODE_DETAILS`, `EDGE_DETAILS` (86 ключей), радиально-секторный layout, координаты ruflo-кластера, `GROUPS`. Метрики карты остаются **83 узла / 90 рёбер / 11 конфликтов**. Добавляется только: 2 константы данных, 1 секция DOM в легенде узла, 2 кнопки в футере, режим `viewMode` в IIFE `HIGHLIGHT`, минимальный CSS.

---

## 4. Фича 1+2 — секция «Паспорт узла» в легенде

Запросы 1 и 2 объединяются в **одну** новую секцию легенды узла (легенда уже из 8 секций; две раздельные сделали бы панель чрезмерно длинной). Секция ставится сразу под бейджем категории (`#legend-category`, строка 96), до «Что делает».

### 4.1. DOM (вставка в `#legend-node-content`)

```html
<div class="legend-section" id="passport-section">
  <h4>📇 Паспорт узла</h4>
  <p><span class="pp-k">Внедрён:</span> <span id="ld-since">—</span></p>
  <p><span class="pp-k">Последнее изменение:</span> <span id="ld-changed">—</span></p>
  <p><span class="pp-k">Использований за 7 дней:</span> <span id="ld-uses">—</span></p>
  <p id="ld-dup-row" style="display:none;"><span class="pp-k">Дубль:</span> <span id="ld-dup">—</span></p>
</div>
```

### 4.2. Рендер (дополнение `showNodeLegend`)

После заполнения категории:

```js
const meta = NODE_META[nodeId] || { since: '—', changed: '—', uses: null, usesSrc: '—' };
document.getElementById('ld-since').textContent   = meta.since || '—';
document.getElementById('ld-changed').textContent = meta.changed || '—';

const usesEl = document.getElementById('ld-uses');
if (meta.uses === null || meta.uses === undefined) {
  usesEl.textContent = 'нет данных (узел не вызывается напрямую)';
  usesEl.style.color = '#586e75';
} else {
  usesEl.innerHTML = `<b>${meta.uses}</b> <span style="color:#839496;">(${meta.usesSrc}) · срез ${META_WINDOW}</span>`;
  usesEl.style.color = '';
}

// строка «Дубль» — парный дубль (DUPLICATE_GROUPS) либо ролевая заметка ruflo (dupNote)
const dupRow = document.getElementById('ld-dup-row');
const dupEl  = document.getElementById('ld-dup');
const pair   = DUP_BY_NODE.get(nodeId);
if (pair) {
  const partnerNode = NODES.find(n => n.id === pair.partner);
  const partnerLabel = partnerNode ? partnerNode.label.replace(/\n/g, ' ') : pair.partner;
  dupEl.innerHTML = `⧉ <b>${partnerLabel}</b> (~${pair.pct}%) — ${pair.reason}`;
  dupRow.style.display = '';
} else if (meta.dupNote) {
  dupEl.innerHTML = `⧉ ${meta.dupNote}`;
  dupRow.style.display = '';
} else {
  dupRow.style.display = 'none';
}
```

### 4.3. CSS

```css
.legend-section p .pp-k { color: #839496; }
#passport-section p { font-size: 12px; color: #eee8d5; line-height: 1.6; }
```

---

## 5. Методика фактических чисел

Значения `since` / `changed` / `uses` производятся на этапе реализации (writing-plans). Спецификация фиксирует **метод**, чтобы числа были воспроизводимы и честны.

### 5.1. `uses` — вызовов за 7 дней (окно 09–16.05.2026)

Источник — транскрипты сессий Claude Code `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\*.jsonl` (≈37 файлов в окне) и git-история.

| категория узлов | как считается | `usesSrc` |
|---|---|---|
| скилы Superpowers (14) + проекта (2) | число вызовов `Skill` с соответствующим значением `skill` | `скил` |
| агенты (11) | число вызовов `Agent` с соответствующим `subagent_type` | `агент` |
| MCP-серверы (7) | число вызовов инструментов `mcp__<сервер>__*` | `MCP` |
| memory-файлы (16) | число вызовов `Read` по пути этого файла | `memory-чтение` |
| ruflo MCP | вызовы `mcp__ruflo__*` | `MCP` |
| ruflo Queen / workers / daemon / memory / commands / agents_catalog / plugins | логи демона ruflo + прямая инспекция рантайма (как в iter5) | `инспекция` |
| хук `hk_session` | число сессий в окне (≈37) | `хук` |
| хук `hk_economy` | число промптов с тегом `экономия N%` в транскриптах | `хук` |
| хуки `hk_pre_claude` / `hk_post_md` / `hk_post_schema` | производно: правки CLAUDE.md / `*.md` / `db/schema.sql` в окне; если надёжно не вывести — `uses: null` | `хук` / `—` |
| lefthook jobs (10) | ≈ число коммитов за 7 дней (каждый коммит гонит набор pre-commit; pre-push реже) | `коммиты` |
| узлы-правила (4: pravila, claude_md, psr_v1, tooling) | напрямую не вызываются | `uses: null` |
| плагины-обёртки (superpowers, fd_plugin, upm, hookify_plugin) | сам плагин не вызывается — работают через скилы; `uses: null`, кроме `claude_md_mgmt` (считается как скил) | `null` / `скил` |

Различие `0` против `null` обязательно: `0` ставится только узлу, чьё использование измеримо и было нулевым (например, idle-воркеры ruflo); `null` — узлу, чьё использование измерить нельзя.

### 5.2. `since` / `changed` — даты

| класс узла | `since` (внедрён) | `changed` (последнее изменение) |
|---|---|---|
| внедрённые в проект (хуки, lefthook jobs, memory-файлы, проектные скилы/агенты, MCP-серверы, ruflo-узлы, документы-правила, claude-md-mgmt) | первый git-коммит / запись в changelog / Tooling, вводящие узел | последний git-коммит, тронувший файл/конфиг узла; для документов-правил — дата последнего бампа версии |
| пришедшие со связкой инструментов (12 скилов Superpowers, встроенные агенты Explore/Plan/general-purpose/guide/statusline + plugin-dev/hookify-агенты, плагины Superpowers/FD/UPM/hookify) | дата подключения связки к проекту (установка Superpowers / FD plugin) | последний проектно-значимый бамп версии связки либо `'—'` |
| без проектного события | `'—'` (можно с фразой `'встроено в Claude Code'`) | `'—'` |

Все даты — из реальной истории (git / changelog / memory), не выдуманные. Узлы без надёжного источника получают честное `'—'`.

---

## 6. Фича 3 — анализ дублей

Критерий заказчика: узлы, **явно** дублирующие друг друга на ≥80% по функции.

### 6.1. Явные парные дубли — D-группы (попадают в кнопку подсветки)

| # | пара | % | обоснование |
|---|---|---|---|
| D1 | `hk_post_md` ⇄ `lh_mdlint` | ~90 | оба — `markdownlint --fix` на `.md`; разница только в триггере (PostToolUse в сессии / lefthook при коммите). Карта уже подписывает ребро «дублируют задачу» |
| D2 | `lh_gitleaks` ⇄ `lh_gitleaks2` | ~85 | оба — скан секретов gitleaks; pre-commit (staged) против pre-push (вся история). pre-push — функциональное надмножество |
| D3 | `sk_rls` ⇄ `ag_rls` | ~85 | оба — 7-пунктовая проверка RLS-соответствия. Уже нарисован 🔴-конфликт «нет регламента кто когда» |
| D4 | `sk_eplans` ⇄ `sk_subagent` | ~80 | оба исполняют план writing-plans пошагово; разница — параллельная сессия против суб-агентов в текущей. Уже есть ребро «альтернатива» |
| D5 | `sk_wplans` ⇄ `ag_plan` | ~80 | оба производят план задачи; скил — inline с полным кодом, агент Plan — архитектурный набросок READ-ONLY. NODE_DETAILS уже отмечает «решают похожую задачу в разных контекстах» |
| D7 | `ruflo_recall_hook` ⇄ `hk_session` | ~80 | оба инжектят память в контекст Клода; recall-хук — на каждый промпт из базы ruflo, SessionStart — при старте из `.md`-файлов |

Итого 6 пар, 12 узлов.

### 6.2. Ролевая избыточность ruflo — R-группы (только заметка, НЕ в кнопке)

Это настоящее дублирование, но один узел дублирует роль **целой подсистемы** — подсветка раздула бы граф до ~40 узлов и потеряла читаемость. Решение заказчика: показать заметкой `dupNote` в легенде трёх ruflo-узлов, в кнопку не включать.

| # | узел | дублирует роль | заметка `dupNote` |
|---|---|---|---|
| D6 | `ruflo_memory` | 16 проектных memory-файлов | «дублирует роль 16 memory-файлов проекта — постоянная память между сессиями; уже ⚫-конфликт с project_state» |
| D8 | `ruflo_commands` | 14 скилов Superpowers | «88 slash-команд дублируют роль скилов — именованные вызываемые процедуры; команды инертны» |
| D9 | `ruflo_agents_catalog` | группа из 11 агентов | «100 определений агентов дублируют реестр агентов; каталог буквально содержит 2 проектных агента» |

(`D6` здесь — отсюда пропуск в нумерации D-групп §6.1.)

### 6.3. Рассмотрено и отклонено

- `ag_explore` против `ag_general` — специализация, не дубль (~70%): Explore — дешёвый поиск только на чтение, general-purpose — дорогой многошаговый. Карта намеренно их разводит.
- `fd_plugin` / `upm` / `mcp_21st` — пересекаются по домену (UI), но роли различны (решатель / материал-библиотека / генератор шаблонов). Уже 🟢-конфликты по R14.5. Не функциональный дубль.
- `claude_md` против `tooling` — оба «оперативная карта уровня 2», но содержание различно (карта проекта против реестра инструментов). ~50%.

---

## 7. Две кнопки

Обе — в нижнюю панель `#cat-legend`, отдельным мини-рядом после 13 бэйджей категорий, со стилем toggle-кнопок (`.cat-ctl`).

### 7.1. DOM (дополнение `#cat-legend`)

```html
<span class="cat-ctl-sep"></span>
<button class="cat-ctl" id="cat-ctl-heat" title="Подсветить узлы по числу вызовов за 7 дней">🔥 По использованию</button>
<button class="cat-ctl" id="cat-ctl-dup"  title="Подсветить явные пары дублей (D1–D5, D7)">⧉ Дубли</button>
```

### 7.2. CSS

```css
.cat-ctl-sep { width: 1px; align-self: stretch; background: #586e75; margin: 0 4px; }
.cat-ctl {
  background: #002b36; border: 1px solid #586e75; color: #93a1a1;
  border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer;
  transition: background 0.12s, box-shadow 0.12s; user-select: none;
}
.cat-ctl:hover { background: #0d4a5a; color: #fdf6e3; }
.cat-ctl.active {
  background: rgba(253,246,227,0.12);
  box-shadow: inset 0 0 0 1px rgba(253,246,227,0.4);
  color: #fdf6e3;
}
```

### 7.3. Поведение — режим `viewMode` в IIFE `HIGHLIGHT`

В `state` (строки 1643–1646) добавляется `viewMode: null` со значениями `null` / `'heat'` / `'dup'`. Режимы **взаимоисключающие**: включение одного выключает другой и сбрасывает `legendFilter` + `selectedNode` (режим — отдельная глобальная картина, не комбинируется с пофильтровой подсветкой).

`computeNodeOpacity` получает приоритетную ветку в начале:

```js
if (state.viewMode === 'heat') return heatOpacity(nodeId);
if (state.viewMode === 'dup')  return DUP_NODE_SET.has(nodeId) ? OPACITY_FOCUS : OPACITY_DIM;
```

`computeEdgeOpacity` в режимах остаётся на `Math.min(fromO, toO)` — без conflict-boost.

**Теплокарта `heatOpacity`** — 4 яруса по `NODE_META[id].uses`; пороги уточняются в плане по фактическим значениям, ориентир:

| ярус | условие | opacity |
|---|---|---|
| часто | `uses` ≥ 21 | 1.0 |
| иногда | 6 ≤ `uses` ≤ 20 | 0.65 |
| редко | 1 ≤ `uses` ≤ 5 | 0.35 |
| простаивает | `uses` === 0 | 0.12 |
| нет данных | `uses` === null | 0.5 (нейтрально) |

Узлы верхнего яруса дополнительно получают `borderWidth: 4` для акцента (сбрасывается при выходе из режима).

Кнопки подключаются в существующий делегат `#cat-legend` click (строки 1753–1759): если клик по `.cat-ctl` — переключить `viewMode`, иначе — прежняя логика `.cat-item`.

`clearAll` (строки 1738–1741) дополняется сбросом `viewMode = null`; кнопка «✕ Снять выделение» уже зовёт `clearAll` — отдельная правка не нужна. `updateLegendVisuals` дополняется снятием/постановкой класса `.active` на двух `.cat-ctl`. При выходе из heat-режима `borderWidth` всех узлов возвращается к 2.

---

## 8. Честность и устаревание

- Секция «Использований за 7 дней» в каждой легенде явно несёт «срез 09–16.05.2026».
- `uses: null` рендерится как «нет данных», не как «0».
- Числа — снимок на дату генерации; статичный HTML их не обновляет. Это согласовано с заказчиком (Вариант A) и соответствует природе карты (поля `when`/`limits` так же правятся вручную каждую итерацию).
- Memory-файл `project_automation_map.md` фиксирует iter6 и дату снимка, чтобы при следующих итерациях было видно, что данные требуют обновления.

---

## 9. Критерии приёмки

1. Открытие `docs/automation-graph.html` двойным кликом: 83 узла / 90 рёбер рендерятся, 0 JS-ошибок (кроме внешнего favicon-404).
2. Клик по любому из 83 узлов: секция «📇 Паспорт узла» показывает `Внедрён` / `Последнее изменение` / `Использований за 7 дней`; узлы из D-групп дополнительно показывают строку «Дубль» с партнёром; 3 ruflo-узла из R-групп — строку «Дубль» с заметкой `dupNote`.
3. Узлы-правила и плагины-обёртки показывают «нет данных», а не «0».
4. Кнопка «🔥 По использованию»: включается/выключается, узлы перекрашиваются по 4 ярусам прозрачности; повторный клик возвращает.
5. Кнопка «⧉ Дубли»: подсвечивает ровно 12 узлов из D1–D5, D7; остальное гаснет.
6. Кнопки взаимоисключающие; «✕ Снять выделение» сбрасывает оба режима и `borderWidth`.
7. `NODE_META` покрывает все 83 id; нет узла без записи.
8. Существующая интерактивная подсветка (iter3), edge-легенда, resize-панель, поиск — без регрессий.

---

## 10. Самопроверка спецификации

- Заглушек/TODO нет: методика дат и чисел задана явно (§5), значения производятся в плане.
- Внутренняя согласованность: нумерация дублей `D1–D5, D7` (пары) + `D6, D8, D9` (роли) — пропуски объяснены в §3.3 и §6.2.
- Объём: одна когезивная итерация одного файла, аддитивная; декомпозиция не нужна.
- Неоднозначности сняты: «легенда узла» = правая панель, «низ легенды» = футер `#cat-legend` (§1); `0` против `null` (§3.2, §5.1); набор кнопки «Дубли» = только D-группы (§6.1, решение заказчика).
