# 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. - **Обязательный `` тег в каждом ответе:** нагрузка на каждый ход, легко забывается. Опираемся на пассивные сигналы из данных (§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`.