# Brain factor-analysis completeness — design

**Дата:** 2026-05-30
**Статус:** design (утверждён заказчиком, ожидает вычитки)
**Автор:** controller (Opus) через `superpowers:brainstorming`
**Тип:** infrastructure — наблюдатель «мозга» (ADR-011). НЕ tooling-канон, НЕ новый ADR, НЕ off-phase подкатегория.

## 1. Проблема

Аудит реальных данных наблюдателя (`docs/observer/episodes-2026-05.jsonl`, 709 эпизодов
schema v4 + 5 observer_error markers, май 2026) показал: схема эпизода богатая
(~35 факторных осей × исход, 4 «прохода», cuts 8-11, tables 16-17), но
**три измерения наполняются неверно или мёртвы**, а **движок срезов умеет только
один фактор против исхода** — нельзя скрестить два фактора.

Источник истины замеров (grep по реальному файлу 2026-05-30):

| Сигнал | Факт | Норма |
|---|---|---|
| `review:{` (вердикт ревьюера) записан | **0 / 714** | должно наполняться по содержательным |
| `outcome:"unknown"` | **709 / 709** (100%) | by design на записи; уточняется ревьюером |
| `outcome_reviewed` заполнен (не null) | **0** (339 имеют поле = null) | должно наполняться |
| `node_chosen:"direct"` | 651 (92%) | — (наблюдение, не проблема) |
| `degraded_mode:true` | **4 / 714** | занижено — тихие откаты LLM→regex не считаются |
| `triggers_matched:[]` (пусто) | **630 / 714** (88%) | ложно-отрицательно |
| `boundaries_applied:[]` (пусто) | **641 / 714** (90%) | ложно-отрицательно |
| `candidates_considered:[]` (пусто) | **611 / 714** (86%) | ложно-отрицательно |
| `prompt_embedding_base64:null` | **705 / 714** (99%) | оживает сама вперёд (не баг) |
| `user_chose_from_options` | 125 (18%) | **проверено: ложных срабатываний НЕТ** |

## 2. Что сознательно НЕ делаем (YAGNI)

- **FP «выбор из вариантов» (#4):** просмотрены все 125 эпизодов — каждый реальный
  выбор из 2-3 предложенных вариантов (часто с меткой «(Рекомендуется)»). Детектор
  требует ≥2 реальных варианта в предыдущем сообщении ассистента, поэтому «делай»/«а»/«б»
  на свободный текст не считаются выбором. **Чинить нечего.**
- **Бэкфилл эмбеддингов (#5):** механизм рабочий (`embedding-mode` + `computeEmbeddingForEpisode`),
  наполнится сам вперёд за пару недель. Прогон модели по 705 эпизодам ради одной оси — YAGNI.
- **Обязательный `<!-- reasoning -->` тег в каждом ответе:** нагрузка на каждый ход,
  легко забывается. Опираемся на пассивные сигналы из данных (§5). Возврат к тегу —
  только если после Слоя A наполнение дисциплины всё ещё <30%.

## 3. Решение — 4 точечных изменения

Все изменения — в существующих чистых модулях, каждое с юнит-тестами. Новых полей в
схеме эпизода нет (кроме тех, что reviewer уже пишет: `review.*` / `outcome_reviewed`).

### 3.1. Оживить оценку качества (#1) — самое ценное

**Файл:** `tools/brain-retro-batch-reviewer.mjs` (рабочий, но не запускался).

**Изменение:** добавить флаг фильтра «содержательности», чтобы ревьюить не все 709
подряд, а только эпизоды с реальным выбором:
- `primary_rationale.node_chosen !== 'direct'` (был выбран скил/узел), ИЛИ
- `task_size.tool_calls >= 20` (крупная задача — medium/large bucket).

Эпизоды-болтовня (direct + мелкие) пропускаются — экономит в разы (содержательных ~60-90 из 709).

Reviewer пишет обратно построчно: `review.*` (8 полей) + `outcome_reviewed` +
`outcome_reviewed_source = "direct_api_batch"`. Это:
- даёт фактор-анализу настоящий столбец «исход» вместо эвристики `inferOutcome` по тону;
- оживляет 2 мёртвых среза STATUS: `computeReviewerFindingsBlock`, `buildRouterVsOpus`.

**CLI:** `--meaningful-only` флаг (по умолчанию выкл для обратной совместимости).

### 3.2. Починить флаг деградации (#2)

**Файл:** `tools/router-classifier.mjs`, ветка `if (!llmResult)` (строки ~653-664).

**Корень:** `degraded: true` стоит только в `catch` (транспортная ошибка, строка 649).
Ветка `if (!llmResult)` обрабатывает ОБА случая отката на regex и НЕ ставит `degraded`:
- `metrics != null` → LLM был вызван, но вернул мусор → `llm_error_type: 'parse_null'`
  — это **настоящая тихая деградация**;
- `metrics == null` → LLM вообще не вызывался (ключа нет) → `llm_error_type: 'no_key'`
  — это **штатный** regex, НЕ деградация.

Вживую: эпизод 713 — `llm_error:"parse_null"`, `latency_ms:12886`, но `degraded_mode:false`.

**Изменение (razor-точное):** в ветке `if (!llmResult)` добавить
`degraded: metrics ? true : false`. То есть `parse_null` → `degraded:true`,
`no_key` → `degraded:false`. Транспортный `catch` (уже `degraded:true`) — не трогаем.

**Граница (критично — не переусердствовать):** «чистый» regex-путь, где LLM не
вызывался (prefilter / cache hit / **нет ключа**), **обязан остаться** `degraded:false`.
Деградация — это *откат после неудачи вызова*, не *штатный отказ от вызова*. Признак
различения уже в коде — `metrics` (непуст ⇔ LLM реально звали).

**Тесты:** (1) `!llmResult` + `metrics` непуст (`parse_null`) → `degraded:true`;
(2) `!llmResult` + `metrics == null` (`no_key`) → `degraded:false`;
(3) prefilter-путь (return до llmCall) → объект без `degraded` (как сейчас).

### 3.3. Починить извлечение дисциплины (#3) — «полно»

**Файлы:** `tools/observer-transcript-parser.mjs` (Слой A), `tools/discipline-metrics.mjs` (Слой B).

**⚠️ Конфликт интересов (зафиксирован явно).** Контроллер (я) предлагает изменение,
которое меняет то, КАК измеряется его собственная дисциплина. «Щедрое» извлечение
рискует *раздуть видимую дисциплину*, а ослабление watchdog — *приглушить сигнал моего
дрейфа* (Pravila §16.4). Митигация ниже разделяет честную и производную метрики и
**не глушит** надзор `/brain-retro`.

**Слой A — расширить извлечение, НЕ подменяя честную метрику:**
- `triggers_matched` / `boundaries_applied` **остаются сырыми** — только дословные
  цитаты правил (`Pravila §N`, `ADR-N`, `PSR_v1 R*`, `routing-off-phase L*`). Это
  по-прежнему честный измеритель «процитировал ли я правило». К ним добавляется только
  расширение по тем же ДОСЛОВНЫМ цитатам (имена узлов `superpowers:*`/`#NN`,
  `hard-floor`/`hard-rule` в формах) — это всё ещё текст моего ответа, не подмена.
- **Объективные признаки роутинга идут в ОТДЕЛЬНОЕ производное поле**
  `primary_rationale.routing_signals` (новое, вычисляется анализатором из уже-имеющихся
  `chain_ref` / `classifier_output.recommended_node`) — НЕ вливаются в `triggers_matched`.
  Метрика дисциплины «% с триггер-матчем» считается по сырому полю и остаётся
  несмягчённой; рядом показывается «% с объективным сигналом роутинга» как отдельный
  столбец. Два числа не смешиваются.

**Слой B — watchdog НЕ ослаблять, а сделать честным** (`routerStepReached`):
- НЕ поднимать порог так, чтобы флаг реже срабатывал. Вместо этого переименовать смысл:
  текущий текст «suspicious — вероятный sentinel-bug парсера» вводит в заблуждение
  (списывает низкую дисциплину на баг). Меняем формулировку на честную:
  «низкая текстовая дисциплина роутинга ИЛИ отсутствие объективных сигналов» — и флаг
  **остаётся видимым** в STATUS при том же пороге (>90% step=1 И пусты объективные
  сигналы). То есть надзор за мной не слабее, а точнее.

**Эффект:** честная метрика «цитирую ли правила» остаётся строгой и не раздувается;
рядом появляется честный объективный сигнал; watchdog не приглушён.

**Живой пример слепого пятна (из этой же сессии, 2026-05-30).** Ход «проанализируй спек
на баги и соседей» контроллер пометил `coverage: direct:spec-adversarial-audit` — хотя по
сути это был шаг **Spec Self-Review** уже-активного `superpowers:brainstorming` (его
чеклист прямо включает перечитывание спека на баги/противоречия/scope). В эпизоде этот ход
ляжет как `node_chosen:direct`, `task_classification:analysis`, `triggers_matched:[]` —
выглядит как «дисциплина провалилась, пошёл напрямую». Классификатор тоже не распознал
(`task_type:conversation`). По факту скил был применён (brainstorming держался весь поток),
просто ни пассивное извлечение, ни классификатор этого не увидели. Это ровно тот случай,
который §3.3 (отдельное поле `routing_signals`) и §3.1 (judged-исход вместо догадки)
призваны сделать видимым: «direct по метке» ≠ «без процесса по сути».

### 3.4. Pivot-функция «любые срезы» (#4 из вопроса заказчика)

**Файл:** `tools/brain-retro-analyzer.mjs`.

**Проблема:** `buildFactorMatrix` умеет только один фактор × исход. Нельзя скрестить два
(«direct-выборы *при экономии 0%*», «переделки *после сжатия контекста*»).

**Изменение:** добавить чистую функцию
`crossTab(episodesWithOutcome, factorA, factorB)` →
`{ "valA|valB": { success: n, rework: n, ... } }`, переиспользуя готовые `FACTOR_FNS`
(35 осей уже определены). Опционально третья ось `factorC` для тройного среза.

`/brain-retro` skill получает возможность выдать любую таблицу «ось × ось → исход» по
запросу, без новых полей в эпизодах.

**Баг B — движок исхода должен предпочитать судейский вердикт (критично).** Сейчас и
`buildFactorMatrix`, и новый `crossTab` читают `_inferredOutcome` — догадку `inferOutcome`
по тону следующего промпта. После §3.1 у содержательных эпизодов есть настоящий
`outcome_reviewed` (вердикт ревьюера), но движок его НЕ использует — значит «любые срезы»
останутся по догадке, и §3.1 наполовину бессмыслен. **Изменение:** ввести единый хелпер
`resolvedOutcome(ep) = ep.outcome_reviewed ?? ep._inferredOutcome ?? 'unknown'` и
использовать его И в `buildFactorMatrix`, И в `crossTab`. Судейский вердикт перекрывает
эвристику, эвристика — фолбэк для неотревьюенных.

**Порядок:** §3.1 (наполнить `outcome_reviewed`) → §3.4 (движок предпочитает его).

**Тесты:** (1) crossTab по двум осям на фикстуре даёт корректное распределение
комбинаций; (2) неизвестная ось → пустой результат, не throw; (3) `resolvedOutcome`
берёт `outcome_reviewed` когда есть, иначе `_inferredOutcome`; (4) `buildFactorMatrix`
на эпизоде с `outcome_reviewed:'rework'` + `_inferredOutcome:'success'` относит его к
`rework`.

## 4. Архитектура, риск и проверка на соседей

- Все 4 изменения — точечные правки в чистых (pure) модулях, покрытых vitest.
- Новое поле схемы — одно: `primary_rationale.routing_signals` (производное, вычисляется
  анализатором, не валидируется как обязательное в Stop-hook). `outcome_reviewed` /
  `review.*` reviewer уже умеет писать. Остальное — без новых полей.
- Порядок зависимости: §3.1 (исход) → §3.4 (движок предпочитает `outcome_reviewed`).
  §3.2 и §3.3 независимы.
- Риск регрессии низкий: модули изолированы, юнит-тесты на каждое изменение.
- Verify: vitest tools-only GREEN (формула — memory `feedback_vitest_sentinel_recipe.md`).

### 4.1. Не задевает ли соседей (проверено grep по реальному коду 2026-05-30)

- **Блокирующие хуки НЕ затрагиваются.** Ни один enforce-хук не читает
  `degraded` классификатора, `triggers_matched`, `node_chosen` или метрики дисциплины
  для решения о блоке. `router-tool-gate` / `enforce-chain-recommendation` /
  `enforce-coverage-verify` / §17-coverage / routing-gate (`detectMethodDirected` +
  provenance) — все остаются без изменений поведения.
- **`degraded` — омоним, не коллизия.** Слово живёт в `enforce-normative-content-rules`,
  `llm-judge-per-tool`, `enforce-decomposition-detector` — но это fail-open флаг LLM-судьи,
  отдельный объект, с классификаторным `classification.degraded` не пересекается.
- **`enforce-normative-content-rules` импортирует `router-classifier`** — но только
  `callAnthropicAPI` (транспорт), НЕ `classify()` и НЕ `degraded`. Правка §3.2 в `classify()`
  его не задевает.
- **`router-prehook` потребляет `classify()`** и пишет `classification.*` в router-state.
  Добавление `degraded:false` в ветке `no_key` — аддитивно, поле и так читается
  анализатором (`degraded_mode` axis) как `?? false`; никакой потребитель не ломается.

### 4.2. Конфликт интересов в части дисциплины скилов (§3.3)

Контроллер предлагает изменение измерения собственной дисциплины. Снято архитектурно:
честная метрика (`triggers_matched`, сырые цитаты правил) **не смягчается**; объективные
сигналы идут отдельным полем; watchdog **не глушится**, только переформулируется честно.
Принуждение (хуки, §17) не трогается вовсе. Детали — §3.3.

## 5. Что войдёт в план реализации

1. batch-reviewer `--meaningful-only` фильтр (node≠direct ИЛИ tool_calls≥20) + прогон по
   содержательным эпизодам мая → наполнить `outcome_reviewed` / `review.*`.
2. `degraded: metrics ? true : false` в ветке `if (!llmResult)` (razor по `no_key`) + 3 теста.
3. Слой A (extractTriggers/Boundaries расширение по ДОСЛОВНЫМ цитатам + отдельное поле
   `routing_signals`) + Слой B (честный watchdog, порог НЕ ослаблять) + тесты.
4. `crossTab` + хелпер `resolvedOutcome` (предпочитает `outcome_reviewed`) в
   `buildFactorMatrix` И `crossTab` + 4 теста.
5. Регрессия vitest tools-only + (опц.) перегенерация STATUS.md для проверки оживших блоков.

## 6. Нормативка

§0 cross-refs **не меняются** — это infrastructure layer наблюдателя (tools/), не
tooling-канон #1-#86, не ADR, не off-phase подкатегория. По завершении — §6 +абзац /
§9 +entry в CLAUDE.md через `/claude-md-management:revise-claude-md`.
