220 lines
19 KiB
Markdown
220 lines
19 KiB
Markdown
# 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`.
|