Brain-retro #2 (весь май) → кандидат: атрибуция canonical chains L1-L13. Spec + 9-task TDD plan (chain_ref в primary_rationale, C6 sync-контролёр, ретрофилл). Исполнение разблокировано — epic observer-instrument-expansion влит в main. +cspell словарь. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
23 KiB
Observer Canonical Chain Attribution (L1–L13) — Design Spec
Дата: 2026-05-20
Автор: controller Opus 4.7 (через superpowers:brainstorming skill, ответы заказчика → AskUserQuestion)
Базовая ветка: feat/project-migration-redesign (формально) → реальное исполнение в свежем worktree off origin/main ПОСЛЕ закрытия epic-плана 2026-05-20-observer-instrument-expansion v1.1 (20 task).
Триггер: docs/observer/notes/2026-05-20-brain-retro-v2.md — Candidate №2 «атрибуция canonical chains L1–L13 в primary_rationale».
Cross-refs: docs/routing-off-phase.md v1.2 (L1–L13 таблица, строки 84–96); docs/superpowers/specs/2026-05-19-observer-factor-analysis-design.md v1.2 (Layer 2 heuristic capture, triggers_matched уже извлекает routing-off-phase LN — но как trigger, не как attribute узла); ADR-011 (Brain governance anchor).
Schema impact: schema_version остаётся 2 — chain_ref это опциональное поле. Backward-compatible с v1/v2-эпизодами без него.
1. Проблема
В эпизодах наблюдателя (docs/observer/episodes-YYYY-MM.jsonl, schema v2) поле primary_rationale.node_chosen фиксирует какой узел Claude выбрал (например, "superpowers:verification-before-completion" или "laravel-boost" или "direct"). Но нет ссылки на канонические цепочки L1–L13 из docs/routing-off-phase.md v1.2.
Из-за этого /brain-retro не может ответить на вопрос: «насколько часто Claude действительно ходит по правильным маршрутам цепочек?». В ретро-ноте 2026-05-20 секция «Canonical chains L1–L12 hit rate» — пустая: «нет атрибуции chain_ref».
extractTriggers (Layer 2 heuristic из factor-analysis v1.2) уже извлекает упоминания routing-off-phase LN в assistant.text — но это маркер триггера (что Claude упомянул в обосновании), а не атрибуция узла: узел Boost #10 живёт в L7 + L13 независимо от того, упомянул ли Claude routing-таблицу в тексте.
2. Цель
Добавить атрибут chain_ref рядом с node_chosen, который связывает выбранный узел с одной или несколькими каноническими цепочками L1–L13. Это позволит /brain-retro строить гистограмму «L1: N раз, L2: M раз, …, вне цепочек: K раз» по любому периоду.
Граница цели: наблюдатель регистрирует атрибут, не диктует поведение. Если Claude выбрал узел вне L-таблицы — никаких блокировок (Pravila §16.4 «не использован ≠ проблема»).
3. Решения по архитектуре (приняты заказчиком через AskUserQuestion)
| # | Развилка | Выбор | Обоснование |
|---|---|---|---|
| 1 | Что для direct-эпизодов (узел не в L-таблице) |
chain_ref: null |
Честно: L1–L13 — routing-цепочки для сложных задач; простые правки в них не входят. |
| 2 | Узел в нескольких L (Boost в L7+L13, Sentry в L8+L13, adr-kit в L4+L5) | Массив всех цепочек ["L7","L13"] |
Узел действительно в обеих; агрегатор считает обе. |
| 3 | Где живёт маппинг узел → цепочки | Отдельный JSON-файл + тест сверки с routing-off-phase.md, врезанный в lefthook pre-commit (Вариант Б, не А) |
Дрейф ловится агрессивно: коммит, расходящийся с .md, блокируется. Pre-commit + Vitest используют одну логику. |
| 4 | Ретроактивность для 23 v2-эпизодов мая 2026 | Однократный ретрофилл-скрипт | ~30 строк, идемпотентный, даёт честную статистику с 19.05. |
4. Архитектура высокого уровня
К наблюдателю добавляется один слой — «chain attribution». Работает в трёх местах:
- При записи эпизода (Stop-хук → парсер транскрипта): после выбора
node_chosenпарсер дополнительно вычисляетchain_refчерез чистую функциюchainsFor(node). Запись в JSONL — атомарная append-line, как и сейчас. - При коммите (lefthook pre-commit): контролёр C6 сверяет JSON-маппинг с таблицей L1–L13 в
routing-off-phase.md. Расхождение → коммит блокируется с человеко-читаемым diff'ом. - При ретро (
/brain-retro):brain-retro-analyzer.mjsагрегирует гистограммуfactorMatrix.chain_ref+chainHitRate. Шаблонaggregation-template.mdзаполняет секцию «Canonical chains L1–L13 hit rate».
Существующие парсер/Stop-хук/routing-detector/choice-detector — нетронуты. Схема schema_version: 2 сохраняется: chain_ref опционален; старые эпизоды без него по-прежнему валидны.
5. Компоненты
| # | Файл | Роль | Тип | Объём |
|---|---|---|---|---|
| 1 | tools/observer-chain-map.json |
Таблица узел → массив цепочек, например {"laravel-boost":["L7","L13"], "superpowers:verification-before-completion":[]}. Человеко-читаемый |
новый | ~60 строк |
| 2 | tools/observer-chain-detector.mjs |
Чистая функция chainsFor(nodeChosen) → массив или null. Грузит JSON один раз, кэширует в Map |
новый | ~40 строк |
| 3 | tools/observer-transcript-parser.mjs |
Точка врезки: в формировании primary_rationale (текущая строка 450) — добавить chain_ref: chainsFor(node_chosen) |
edit existing | +2 строки |
| 4 | tools/observer-chain-map-checker.mjs |
Контролёр C6: парсит таблицу L1–L13 из routing-off-phase.md, сверяет с JSON-маппингом. Возвращает diff или OK |
новый | ~80 строк |
| 5 | lefthook.yml |
Новый job chain-map-sync в pre-commit запускает контролёр C6. Блокирует коммит при расхождении |
edit existing | +5 строк |
| 6 | tools/observer-retrofill-chain-ref.mjs |
Одноразовый скрипт: добавляет chain_ref к v2-эпизодам в episodes-*.jsonl. Атомарный (tmp + rename), идемпотентный |
новый | ~30 строк |
| 7 | tools/brain-retro-analyzer.mjs |
Дополняется factorMatrix.chain_ref (гистограмма) и chainHitRate (массив с процентами) |
edit existing | +20 строк |
| 8 | .claude/skills/brain-retro/references/aggregation-template.md |
Раздел «Canonical chains L1–L13 hit rate» заполняется реальными данными из аналитика | edit existing | +5 строк |
Тесты (отдельно, 2 файла):
tests/observer-chain-detector.test.mjs— юнит-тестыchainsFor: известный узел / multi-chain узел / неизвестный узел /direct/null/undefined/ пустая строка. ~6 тестов, ~50 строк.tests/observer-chain-map-sync.test.mjs— интеграционный тест: запускает тот же C6-чекер, парситrouting-off-phase.md, сверяет с JSON. Та же логика что в pre-commit, один источник правды.
Итого: 4 новых файла в tools/, 2 новых теста, 4 точки правок в existing, +1 в lefthook.yml, +1 в SKILL-template.
6. Поток данных
Поток A — запись эпизода (runtime, при каждом Stop-хуке)
Stop-хук → observer-transcript-parser.mjs (existing)
↓
формирует primary_rationale (node_chosen, triggers_matched, …)
↓
вызывает chainsFor(node_chosen) ← из observer-chain-detector.mjs (NEW)
↓ читает observer-chain-map.json (cached)
primary_rationale.chain_ref = ["L7","L13"] | null
↓
append-line в docs/observer/episodes-YYYY-MM.jsonl
Дополнительная задержка — ~1–2 мс (lookup в Map<string, string[]> в памяти). JSON загружается один раз при первом вызове.
Поток B — pre-commit сверка (раз в коммит)
git commit → lefthook → job chain-map-sync (NEW)
↓
node tools/observer-chain-map-checker.mjs
↓
parse routing-off-phase.md (таблица L1–L13, строки 84–96)
↓
load observer-chain-map.json
↓
diff: (узлы в .md без записи в JSON) ∪ (узлы в JSON без записи в .md) ∪ (L-список расходится)
↓
exit 0 (синхронно) | exit 1 (расхождение + human-readable сообщение с подсказкой)
Поток C — агрегация в /brain-retro (раз в спринт)
node tools/brain-retro-analyzer.mjs episodes-*.jsonl
↓
читает все эпизоды (v2)
↓
группирует по chain_ref (multi-chain эпизоды засчитываются в каждую L)
↓
factorMatrix.chain_ref: {"L1":0, "L7":3, "L13":1, "null":19, …}
↓
chainHitRate: [{chain:"L7", times:3, percent:"13.0%"}, …]
↓
aggregation-template заполняет секцию «L1–L13 hit rate»
Поток D — однократный ретрофилл
node tools/observer-retrofill-chain-ref.mjs [--dry-run]
↓
для каждого episodes-*.jsonl:
↓ для каждой строки v2:
если chain_ref уже есть → skip (idempotent)
иначе: добавить chain_ref: chainsFor(node_chosen)
↓
атомарная перезапись (write tmp → rename)
Запускается один раз вручную после внедрения. Повторный запуск — 0 changes.
Что НЕ меняется:
schema_version: 2—chain_refопциональное поле.- 5 v1-эпизодов мая 2026 — пропускаются (нет
schema_version: 2и нетprimary_rationale). - Stop-хук, parser-логика выбора
node_chosen, routing-detector, choice-detector — не трогаем. - Никаких новых hard-blockers для Claude. Pre-commit блокирует только дрейф маппинга.
7. Обработка ошибок
| # | Сценарий | Поведение |
|---|---|---|
| 1 | Узел не найден в маппинге (chainsFor("foo-bar-baz")) |
Возвращает null. Эпизод пишется. Нормальная ситуация для direct/новых skills/строкового шума |
| 2 | observer-chain-map.json отсутствует или битый |
Парсер ловит исключение, пишет observer_error маркер + chain_ref: null. Эпизод всё равно записывается — наблюдатель не падает. Контролёр C5 (observer-coverage-checker) увидит маркер и подсветит в STATUS.md |
| 3 | .md правят, JSON не обновили (дрейф) |
Pre-commit chain-map-sync падает с понятным diff'ом: «в .md есть ru-tax-accounting → L13, в JSON нет — добавьте». Узлы из новых L-цепочек до добавления в JSON не атрибутируются (см. п. 1) |
| 4 | Формат таблицы в .md изменён (новая колонка, переименование L) |
Парсер chain-map-checker.mjs падает с указанием строки: «не могу распарсить L7». Vitest даёт ту же ошибку до коммита |
| 5 | Retrofill-скрипт прерван на середине | Запись атомарная (tmp + rename) — файл всегда консистентен. Идемпотентность — повторный запуск пропускает уже обработанные строки |
| 6 | JSON ссылается на несуществующую L (например L99) |
C6 ловит: «JSON ссылается на L99, в .md нет». exit 1 |
| 7 | v1-эпизоды (5 строк мая) | Пропускаются — у них нет schema_version: 2 и primary_rationale |
8. Тестирование
Юнит-тесты (tests/observer-chain-detector.test.mjs):
chainsFor("laravel-boost")→["L7","L13"]chainsFor("superpowers:verification-before-completion")→null(узел НЕ входит ни в одну L1–L13 → его нет в JSON-маппинге →null, как и дляdirect)chainsFor("direct")→nullchainsFor("unknown-node")→nullchainsFor(""),chainsFor(null),chainsFor(undefined)→null- ~6 тестов, ~50 строк.
Единообразие формы (решено в self-review): только две формы результата — непустой массив ["LN", …] (узел есть в JSON) или null (узла нет в JSON: direct / узел вне цепочек / неизвестный / шум). Пустой массив [] НЕ используется — нет узлов «в маппинге, но без цепочек».
Интеграционный тест синхронизации (tests/observer-chain-map-sync.test.mjs):
- Использует тот же
chain-map-checker.mjs, что и pre-commit. - Падает, если кто-то добавил строку в
.mdбез обновления JSON. - Сообщение теста = сообщение pre-commit job.
Сценарный тест парсера (опционально, если бюджет позволяет):
- Подаёт фиктивный транскрипт с известным skill (
Boost) → проверяет, что в результирующем эпизоде естьchain_ref: ["L7","L13"]. - Подаёт транскрипт без skill (direct) →
chain_ref: null. - Подаёт с битым JSON → пишется
observer_error+chain_ref: null.
Smoke ретрофилла (ручной шаг после реализации):
node tools/observer-retrofill-chain-ref.mjs --dry-run→ видим планируемые изменения.- Если OK — без
--dry-run. Идемпотентность проверяется повторным запуском (должно быть 0 changes).
Что НЕ тестируем:
- Скорость (1–2 мс заведомо некритично для Stop-хука).
- Все 60+ узлов руками — JSON + sync-тест уже это покрывают.
9. Initial JSON-маппинг (на момент дизайна, routing-off-phase.md v1.2)
Источник истины — таблица L1–L13 в docs/routing-off-phase.md строки 84–96. Извлечение узлов из ячейки «Цепочка» — однократное руками при первой реализации, дальше контролёр C6 синхронизирует.
Примерный shape (демонстрация формата; финальный JSON генерируется при реализации):
{
"_note": "Только узлы, входящие хотя бы в одну L1-L13. Узлы вне цепочек (direct, verification-before-completion, прочие skills вне L) НЕ включаются — chainsFor вернёт null.",
"discovery-interview": ["L1","L2"],
"superpowers:brainstorming": ["L1"],
"superpowers:writing-plans": ["L1"],
"superpowers:subagent-driven-development": ["L1"],
"audit-portal": ["L2"],
"process-analysis": ["L3"],
"process-modeling": ["L3","L4"],
"mermaid": ["L4"],
"adr-kit": ["L4","L5"],
"operations": ["L4"],
"architecture-patterns": ["L5"],
"deptrac": ["L5"],
"trail-of-bits": ["L6"],
"semgrep-mcp": ["L6"],
"security-guidance": ["L6"],
"security-review": ["L6"],
"openapi-mcp-server": ["L7"],
"api-docs": ["L7"],
"laravel-boost": ["L7","L13"],
"superpowers:systematic-debugging": ["L8"],
"sentry-mcp": ["L8","L13"],
"redis-mcp": ["L8","L13"],
"ccpm": ["L9"],
"product-management": ["L9"],
"github-mcp": ["L9"],
"promptfoo": ["L10"],
"data-scientist": ["L10"],
"claude-api": ["L10"],
"skill-creator": ["L11"],
"hookify": ["L11"],
"plugin-dev": ["L11"],
"claude-md-management": ["L12"],
"billing-audit": ["L13"],
"pest": ["L13"],
"ru-tax-accounting": ["L13"]
}
Note для реализации: имена узлов в JSON должны точно соответствовать значениям, которые парсер записывает в node_chosen. Если узел в node_chosen — это "superpowers:verification-before-completion", то в JSON ключ такой же. Это валидируется C6 + sync-тестом.
10. Внешние зависимости и порядок исполнения
Жёсткая зависимость: этот spec исполняется ПОСЛЕ закрытия и push'а в origin/main эпик-плана docs/superpowers/plans/2026-05-20-observer-instrument-expansion.md v1.1 (20 атомарных коммитов).
Причина: epic 20-task правит observer-transcript-parser.mjs в Tasks #1, #2, #4, #6, #7, #8, #9, #12, #13. Моя врезка chain_ref тоже в этом файле. Параллельное исполнение → merge-конфликт почти гарантирован.
Процедура запуска работы по этому spec:
- Дождаться push'а epic 20-task на
origin/main(контролёр или владелец сообщает «epic закрыт, push сделан»). git fetch origin && git log HEAD..origin/main --oneline— убедиться, что 20 task реально влиты.- Создать свежий worktree
.claude/worktrees/observer-chain-attribution/offorigin/main(новая веткаfeat/observer-chain-attribution). - Перейти к
superpowers:writing-plans— детальный TDD-план по компонентам §5. - Исполнить через
superpowers:subagent-driven-development(Sonnet/Opus only per Pravila §15.1). - Финальный push:
git push origin feat/observer-chain-attribution:main(FF-merge).
11. Влияние на нормативку
Не требует правок Pravila / PSR_v1 / Tooling / CLAUDE.md / ADR.
chain_ref— опциональное расширение schema v2, не нормативный сдвиг (какtask_costв epic-плане v1.1 — там тоже без правок Pravila).- L1–L13 уже формализованы в
routing-off-phase.mdv1.2 (правлено 20.05 при finance-tooling). - Контролёр C6 — добавление нового lefthook job, по аналогии с C1/C2/C3/C4/C5; в STATUS.md появится строка «C6 Chain map sync». Это организационная правка, не нормативная.
Опционально: при желании заказчика — micro-правка в Pravila §16.2 «Схема эпизода v2» с упоминанием chain_ref как опционального атрибута. Делается одним коммитом через claude-md-management. Не блокирует основную работу.
12. Из scope исключено (NOT this spec)
chain_divergenceevent — заявлен в factor-analysis v1.2 §10 как phase-2 / agent-based (нужен LLM-судья «правильная ли цепочка»). Не в этом spec'е.chain_ref— это атрибутирование, а не суждение.triggers_matched: routing-off-phase L7heuristic — уже реализуется в epic-плане v1.1 Task #6 (reasoning capture).chain_ref— отдельный атрибут параллельно, не дубль.- Real-time блокировка «ушёл вне цепочки» — спорная idea, противоречит Pravila §16.4 «не использован ≠ проблема». NOT this spec.
- Авто-правки нормативки по результатам hit rate — фантазия v3+. NOT this spec.
13. Acceptance criteria
Spec считается реализованным когда:
- Все 6 новых файлов из §5 созданы, тесты проходят локально и в CI.
- Pre-commit job
chain-map-syncврезан вlefthook.yml, smoke-проверка red-green работает (намеренная рассинхронизация JSON ↔ .md → коммит блокируется; правильная синхронизация → проходит). node tools/observer-retrofill-chain-ref.mjs --dry-runпоказывает планируемые изменения для всех v2-эпизодов вepisodes-2026-05.jsonl; запуск без--dry-runдобавляетchain_ref; повторный запуск → 0 changes./brain-retroследующего spring печатает непустую секцию «Canonical chains L1–L13 hit rate» с реальными цифрами.- STATUS.md добавлена строка «C6 Chain map sync: ✅ — last sync OK».
- Финальная регрессия
npm run test:tools≥ 331 + N (где N — число новых тестов из §8) GREEN.