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

19 KiB
Raw Blame History

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_nulldegraded:true, no_keydegraded: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.