# Дашборд мозга — дизайн

**Дата:** 2026-05-19
**Статус:** согласован в brainstorming-сессии, готов к writing-plans
**Источник:** brainstorming-сессия Claude + Дмитрий

## 1. Контекст и проблема

`docs/automation-graph.html` («карта мозга») — статический снимок: 124 узла, 130 рёбер, 11 размеченных конфликтов, плюс ручная теплокарта `NODE_META` за фиксированное окно. Карта показывает *топологию на момент*, но не показывает *работу*: как решались задачи, какие узлы задействованы, где возникли ошибки и ретраи, куда перенаправлялось, где сталкивались узлы.

Журнал исполнения уже существует — `docs/observer/episodes-YYYY-MM.jsonl`: наблюдатель (ADR-011) пишет одну запись на каждый ход (Stop-событие). В записи — выбранный узел, инструменты, ошибки, ретраи, перенаправления, hard-floor, классификация задачи, окружение. **Данные есть — нет визуализации.** Карта и журнал между собой не связаны.

## 2. Цель и не-цели

**Цель:** standalone-дашборд, визуализирующий журнал эпизодов поверх топологии карты — три способа смотреть на работу мозга плюс сама карта.

**В scope:**

- Чтение `episodes-*.jsonl` (схема v1 и v2).
- Четыре view: Карта, Разбор задачи, Лента сессии, Агрегат.
- Граф как общий холст для траекторий и тепла.
- Три слоя «конфликтов» (см. §6).

**Не-цели (YAGNI):**

- Не меняем формат эпизодов и логику наблюдателя (исключение — отдельная задача, §13).
- Не правим `/brain-retro` и контролёры.
- Не пиксель-полировка — Forest накладывается на этапе реализации (frontend-design).
- Нет истории до запуска наблюдателя — её физически нет.
- Не Vue/Vuetify-приложение — это dev-инструмент, zero-build (см. §3).

## 3. Зафиксированные решения

**Тех-модель: standalone HTML + локальный статик-сервер.** Один HTML-файл без сборки; читает свежий JSONL через статик-сервер (`fetch` с `file://` браузер блокирует). Сервер — ~20 строк на `node:http`, запуск npm-командой, гасится по Ctrl-C. Ноль новых npm-зависимостей, ноль постоянных демонов. Отвергнуты: «запекаемый файл» (генератор вшивает данные — лента не живая) и «Vue/Vuetify-приложение» (сборка + стек портала в dev-инструменте; vis.js всё равно не Vue).

**Раскладка: граф баннером сверху.** Постоянная полоса графа сверху, рабочая зона view снизу; переключатель view меняет нижнюю зону. Граф всегда виден; широкие таблицы и ленты снизу.

## 4. Архитектура

### 4.1. Слой данных

Источник — `docs/observer/episodes-YYYY-MM.jsonl`, append-only, одна строка = один эпизод.

Сервер `tools/brain-dashboard-server.mjs`:

- статика из корня репо (HTML, JS, JSONL, `automation-graph.html`, vis.js);
- эндпоинт `GET /api/episodes` → JSON-список имён файлов `docs/observer/episodes-*.jsonl` (дашборд не угадывает имена);
- больше ничего; только localhost.

Парсер (JS, внутри дашборда):

- читает каждую строку JSONL → объект эпизода;
- нормализует **v1** (строки без `schema_version` — нет `decision_provenance` / `environment` / `task_size` / `prompt_signal`, `outcome` уже проставлен) и **v2** (`schema_version: 2`);
- битые строки и строки-маркеры `observer_error` — пропускаются, ведётся счётчик «N пропущено»;
- результат — нормализованный массив эпизодов единой формы (отсутствующие v1-поля → `null`).

Производные данные (тепло, кластеры, агрегаты) считаются в браузере при загрузке. Ноль вшитых данных → всегда свежо.

### 4.2. Карта = общий холст

Сейчас топология зашита константами внутри `automation-graph.html` (один файл ≈2900 строк): `NODES` (стр. 229), `EDGES` (стр. 418), секции, `CONFLICT`-данные (стр. ≈406–614), `NODE_META` (стр. 1898), `NODE_SECTION` (стр. 2155).

**Рефактор-вынос:** константы топологии (`NODES`, `EDGES`, `SECTIONS`, `CONFLICT`-данные, `NODE_SECTION`) выносятся в `docs/automation-graph-data.js`. Старая карта `<script src>`-ит его — поведение и вид не меняются (подтвердить визуальным smoke-тестом). `NODE_META` (ручная теплокарта) **остаётся в старой карте** — дашборд её не использует, он считает тепло из эпизодов.

Дашборд импортирует `automation-graph-data.js` и строит **свой** экземпляр vis.js-графа в баннере. Этот граф управляемый — на нём анимируются траектории (Разбор) и красится тепло (Агрегат). Iframe старой карты отвергнут: чужой iframe нельзя анимировать снаружи.

Вкладка «Карта» = тот же граф дашборда в режиме «без оверлея». Файл `automation-graph.html` продолжает существовать как самостоятельная голая карта.

### 4.3. Атрибуция узлов (честная)

Граф — 124 узла (MCP-серверы, плагины, скилы, инструменты, секции). Эпизод даёт сигналы об узлах:

- `primary_rationale.node_chosen` — чаще всего `"direct"`, иногда id скила (`"superpowers:systematic-debugging"`);
- события `skill_invoked` — id скилов;
- `tool_summary.counts` — имена встроенных инструментов Claude (`Read`, `Edit`, `Bash`, `Grep`, …) и `Skill` / `ToolSearch`.

Маршрутизируемый словарь наблюдателя — `tools/observer-known-nodes.txt` (~22 имени: 13 superpowers-скилов + 7 проектных + 2 плагина/команды). Это **меньше** 124 узлов графа и пересекается с ними частично.

**Решение:** дашборд держит таблицу соответствия `сигнал эпизода → id узла графа`. Подсвечивает узлы, для которых соответствие есть. Узлы без соответствия (встроенные инструменты Claude, большинство MCP/плагинов, которые эпизоды пока не называют) **остаются неподсвеченными** — это ожидаемо и подписано в UI («атрибутировано N из M сигналов»). Полнее станет, когда роутер начнёт писать `node_chosen` детальнее — вне scope этой задачи.

## 5. Четыре view

Общий каркас (раскладка из §3): сверху переключатель view + граф-баннер; снизу — рабочая зона.

### 5.1. Карта

Граф без оверлея: топология + 11 размеченных дизайн-конфликтов (цвета RED / BLACK / GREEN из `CONFLICT_TYPES`). Фильтры по секциям/типам — переносятся из старой карты по возможности (или минимальный набор). Это «нулевое состояние» холста.

### 5.2. Разбор задачи (ретроспектива)

Нижняя зона: слева список эпизодов (фильтр: дата, `task_classification`, `outcome`, `path_type`, наличие ошибок); справа — детали выбранного.

Выбор эпизода → траектория:

- на графе подсвечиваются атрибутированные узлы (§4.3);
- справа — упорядоченный список событий эпизода (`skill_invoked` / `error` / `retry` / `hook_fired` / `interrupt` / `time_burn`), плюс шапка: классификация, `path_type`, `decision_provenance` (если `user_directed_method` — «перенаправление: выбран X, автономно был бы Y»), `hard_floor`, окружение (`economy_level`, `model`, `post_compaction`, `session_turn`, `parallel_session`), `task_size`.

Честно: внутри хода есть упорядоченный список событий, но не каждый tool-вызов по порядку — `tool_summary` даёт только счётчики. «Траектория» = последовательность событий + сводка инструментов.

### 5.3. Лента сессии (живая)

Нижняя зона: одноколоночный поток эпизодов, сгруппированных по `task_id` / `task_ref`, новый ход сверху. Карточка хода: время, классификация, `path_type`, атрибутированный узел, ошибки/ретраи (бейджи), длительность (`ended_at − started_at`), флаг перенаправления.

Автоопрос: дашборд раз в N секунд (по умолчанию 5) перезапрашивает `/api/episodes` + текущий месячный JSONL, дописывает новые строки. Полл — только в этом view. Кнопка пауза/возобновить.

### 5.4. Агрегат (тренды)

Нижняя зона: плитки метрик по всем эпизодам:

- тепло узлов (авто — сколько раз каждый атрибутированный узел встречался); красит граф-баннер;
- горячие точки ошибок/ретраев (узлы и классы задач с наибольшей долей `error` / `retry`);
- доля перенаправлений (`decision_provenance.kind == "user_directed_method"`);
- распределения `economy_level`, `path_type` (improvised/regulated), `task_classification`, `outcome`;
- счётчик `observer_error` и пропущенных строк.

Опциональный reuse: для v2-эпизодов с `outcome: "unknown"` — переиспользовать детерминированный inference из `tools/brain-retro-analyzer.mjs`, если он оформлен импортируемым модулем; иначе показывать `unknown`. Решается в плане.

## 6. Конфликты — три слоя

Запрос — «где конфликты среди узлов». Эпизоды не пишут «узел A столкнулся с узлом B» — только `error` / `retry` / `hook_fired.errors`. Дашборд отдаёт три явно подписанных слоя:

1. **Дизайн-конфликты** — 11 размеченных `CONFLICT`-рёбер карты (факт, из топологии).
2. **Трение** — эпизоды с `error`/`retry`, привязанные к атрибутированным в них узлам. Это инференс («во время хода с этим узлом была ошибка»), не доказанный конфликт. Подписано.
3. **Корреляция** — эпизод с ошибкой, где атрибутированы два узла, между которыми есть `CONFLICT`-ребро → «конфликт мог реализоваться». Эвристика. Подписано.

Настоящего лога «узел×узел» нет. См. §13.

## 7. Раскладка

```
┌─────────────────────────────────────────────┐
│  [Карта] [Разбор] [Лента] [Агрегат]          │  переключатель view
├─────────────────────────────────────────────┤
│                                             │
│            ГРАФ — баннер (vis.js)           │  ~40% высоты, всегда виден
│                                             │
├─────────────────────────────────────────────┤
│                                             │
│         рабочая зона view (меняется)        │  ~60% высоты
│                                             │
└─────────────────────────────────────────────┘
```

Граф-баннер общий для всех view; рабочая зона своя у каждого. Forest-палитра (Teal `#0F6E56`, ivory `#F6F3EC`, теало-нуар `#012019`), Inter / JetBrains Mono — накладываются на этапе реализации как CSS-переменные.

## 8. Файлы и компоненты

| Файл | Назначение | Статус |
|---|---|---|
| `docs/observer/dashboard.html` | каркас дашборда (раскладка из §3/§7, vis.js-граф) | новый |
| `docs/observer/dashboard.js` | парсер JSONL + агрегатор + 4 view + рендер графа | новый |
| `docs/automation-graph-data.js` | вынесенная топология (`NODES` / `EDGES` / `SECTIONS` / `CONFLICT` / `NODE_SECTION`) | новый (вынос) |
| `docs/automation-graph.html` | `<script src>` на data-файл; остальное без изменений | правка |
| `tools/brain-dashboard-server.mjs` | статик-сервер + `/api/episodes` | новый |
| `package.json` | скрипт `brain:dashboard` (запуск сервера + открытие браузера) | правка |
| `tools/brain-dashboard-*.test.mjs` | тесты парсера / агрегатора / сервера | новый |

`dashboard.js` при росте можно разбить на модули (`parser.js`, `aggregate.js`, `graph.js`, `views/*.js`) — решается в плане по фактическому размеру.

## 9. Обработка ошибок и граничные случаи

- Битая JSONL-строка / `observer_error`-маркер → пропуск, инкремент счётчика, показ счётчика в UI.
- Месячный файл отсутствует или пуст → не ошибка.
- Эпизодов нет вообще → дружелюбное пустое состояние.
- v1-эпизод (нет v2-полей) → недостающие поля `null`, UI показывает «—».
- Сервер не запущен → дашборд физически не откроется (он отдаётся этим же сервером); пустой `/api/episodes` → пустое состояние.
- `automation-graph-data.js` не загрузился → пустой граф + явное сообщение.

## 10. Тестирование

- **TDD** на чистую логику (`dashboard.js` — парсер, нормализация v1/v2, агрегатор, атрибуция узлов, инференс конфликтов): `tools/brain-dashboard-*.test.mjs` на `node:test`, failing-first → GREEN. Паттерн — как существующие `tools/*.test.mjs`.
- Сервер `brain-dashboard-server.mjs` — smoke-тест (поднять, дёрнуть `/api/episodes`, проверить отдачу статики).
- Вынос топологии — визуальный smoke старой карты (Playwright или ручной): карта выглядит и фильтруется как до выноса.
- Рендер view и графа — ручной визуальный smoke в браузере.

## 11. Честные ограничения

1. **Гранулярность — один эпизод на ход.** Внутри хода есть упорядоченный список событий, но не каждый tool-вызов по порядку (только счётчики `tool_summary`).
2. **«Живость» — после Stop, не в процессе.** Лента обновляется после завершения хода (+ задержка автоопроса), не пока ход идёт.
3. **Атрибуция узлов частичная** — `node_chosen` чаще `direct`; словарь наблюдателя ~22 имени против 124 узлов графа. Бóльшая часть графа не подсвечивается. См. §4.3.
4. **Конфликты узел×узел не логируются** — даётся инференс (§6), не факт.
5. **История — только с запуска наблюдателя (~19.05.2026).** Раньше эпизодов нет.

## 12. Порядок сборки (3 фазы, один спек)

- **Фаза 1 — фундамент.** Статик-сервер + `/api/episodes`; вынос топологии в `automation-graph-data.js` + правка старой карты + визуальный smoke; каркас `dashboard.html`; парсер v1/v2 + атрибуция узлов (TDD); граф-баннер; view «Карта» + view «Разбор задачи»; npm-скрипт `brain:dashboard`.
- **Фаза 2 — живость.** View «Лента сессии» + автоопрос/пауза.
- **Фаза 3 — агрегат.** View «Агрегат» + тепло на граф-баннер + три слоя конфликтов (§6); опц. reuse `brain-retro-analyzer.mjs` для outcome-инференса.

## 13. Открытые вопросы / отложено

- **Настоящий лог конфликтов узел×узел** — потребует нового типа события в наблюдателе (`tools/observer-transcript-parser.mjs` + схема эпизода). Отдельная задача, не входит в этот спек.
- **Двойной клик без сервера** — если когда-нибудь понадобится: добавить генератору запекание данных, файл выродится в тех-модель «запекаемый файл» без переписывания. Сейчас YAGNI.
- **Forest-полировка** — этап реализации (frontend-design).
- **vis.js** — откуда его берёт старая карта (CDN или вендорено) — уточнить в плане, дашборд переиспользует тот же способ.
