19 KiB
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_modeaxis) как?? false; никакой потребитель не ломается.
4.2. Конфликт интересов в части дисциплины скилов (§3.3)
Контроллер предлагает изменение измерения собственной дисциплины. Снято архитектурно:
честная метрика (triggers_matched, сырые цитаты правил) не смягчается; объективные
сигналы идут отдельным полем; watchdog не глушится, только переформулируется честно.
Принуждение (хуки, §17) не трогается вовсе. Детали — §3.3.
5. Что войдёт в план реализации
- batch-reviewer
--meaningful-onlyфильтр (node≠direct ИЛИ tool_calls≥20) + прогон по содержательным эпизодам мая → наполнитьoutcome_reviewed/review.*. degraded: metrics ? true : falseв веткеif (!llmResult)(razor поno_key) + 3 теста.- Слой A (extractTriggers/Boundaries расширение по ДОСЛОВНЫМ цитатам + отдельное поле
routing_signals) + Слой B (честный watchdog, порог НЕ ослаблять) + тесты. crossTab+ хелперresolvedOutcome(предпочитаетoutcome_reviewed) вbuildFactorMatrixИcrossTab+ 4 теста.- Регрессия 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.