Files
portal/docs/superpowers/specs/2026-05-30-brain-factor-analysis-completeness-design.md
T

220 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.