From af15f24de74217c80a01e5e51f3789db876849f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:35:43 +0300 Subject: [PATCH 01/11] =?UTF-8?q?feat(map):=20A1=20backend-tooling=20?= =?UTF-8?q?=E2=80=94=20NODE=5FDETAILS=20+=20NODE=5FMETA=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20#64-67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Узлы rector/php_insights/backend_patterns/nightowl теперь в панелях описания (nd()) и теплокарте использования (NODE_META, uses:0 новые). Дополняет 5d82fdd (NODES/EDGES/ NODE_SECTION в data.js). Browser-smoke: 141 узел, NODE_META+NODE_DETAILS у всех 4, 0 JS-ошибок. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/automation-graph.html | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/automation-graph.html b/docs/automation-graph.html index 1d09bea4..e160861c 100644 --- a/docs/automation-graph.html +++ b/docs/automation-graph.html @@ -580,6 +580,40 @@ const NODE_DETAILS = { [{ name: 'billing-audit', cond: 'выручка C6 → налог.база C7' }, { name: 'finance plugin', cond: 'US-механика' }] ), + // ── A1 BACKEND-TOOLING (20.05.2026, ADR-013) ── + rector: nd( + 'Composer dev-dep (Rector + rector-laravel): авто-рефакторинг и version-upgrade PHP-кода — dead-code, code-quality наборы, апгрейды под версию Laravel.', + 'При «обнови/почини/рефактори backend-код», апгрейде Laravel-версии, удалении мёртвого кода. Запуск manual/CI (composer rector / rector:fix).', + 'Composer dev-dep, app/rector.php (deadCode+codeQuality conservative). manual/CI — НЕ блокирующий lefthook (dry-run baseline 16 файлов). Не UI → вне R6/R14. Tooling §4.39 #64, CLAUDE.md §3.3 #64, ADR-013.', + [{ name: 'Tooling', cond: '§4.39 #64 — реестр' }], + [{ name: 'BT1', cond: '↔ Pint трансформация vs стиль' }, { name: 'BT2', cond: '↔ Larastan чинит vs находит' }, { name: 'BT3', cond: '↔ deptrac vs граф слоёв' }], + [{ name: 'PHP Insights', cond: 'backend-quality chain L14' }, { name: 'Larastan', cond: 'L14 типы' }] + ), + php_insights: nd( + 'Composer dev-dep: метрики качества кода — complexity / architecture / maintainability (cyclomatic, code smells, распределение архитектуры).', + 'При «оцени качество/сложность кода», «где код запутан», в портальном аудите. on-demand/CI (composer insights).', + 'Composer dev-dep, app/config/insights.php (SyntaxCheck removed — Windows-краш, style-ось off — владелец Pint). on-demand/CI — НЕ блокирующий (BT9). Не UI → вне R6/R14. Tooling §4.40 #65, CLAUDE.md §3.3 #65, ADR-013.', + [{ name: 'Tooling', cond: '§4.40 #65 — реестр' }], + [{ name: 'BT4', cond: 'style/code оси off — уникум complexity+architecture' }, { name: 'BT9', cond: 'не блокирующий — без четверного гейта' }], + [{ name: 'Rector', cond: 'backend-quality chain L14' }, { name: 'Larastan', cond: 'L14 типы' }] + ), + backend_patterns: nd( + 'Self-authored скил: backend-конвенции Лидерры — слоистость controller→service→job, RLS-aware Eloquent, деньги bcmath/LedgerService, идемпотентные джобы, partition-aware запросы.', + 'При «как писать backend в Лидерре», «паттерн контроллера/сервиса/джоба», scaffolding новой backend-фичи.', + 'Свой project-скил .claude/skills/laravel-backend-patterns/ (линтуется, LINT1). Не UI → вне R6/R14. Tooling §4.41 #66, CLAUDE.md §3.3 #66, ADR-013.', + [{ name: 'Tooling', cond: '§4.41 #66 — реестр' }], + [{ name: 'BT5', cond: '≠ architecture-patterns #38 (generic)' }, { name: 'BT6', cond: '≠ billing-audit #62 (аудит)' }], + [{ name: 'billing-audit', cond: '«как писать» ↔ «аудит денег»' }, { name: 'Boost', cond: 'Eloquent-контекст' }] + ), + nightowl: nd( + 'Self-hosted runtime-телеметрия (laravel/nightwatch + nightowl-agent): коррелированный трейс request↔job↔query↔cache в свой PostgreSQL. DEFERRED.', + 'DEFERRED — при появлении Linux/боевого сервера (Б-1). Сейчас не маршрутизировать (нет pcntl/posix на Windows, OSS без MCP, hosted = 152-ФЗ).', + 'DEFERRED pending-слот (как Sentry #34 / Figma #44 / Jupyter #50). Spike docs/backend/nightowl-spike.md. Не UI → вне R6/R14. Tooling §4.42 #67, CLAUDE.md §3.3 #67, ADR-013.', + [{ name: 'Tooling', cond: '§4.42 #67 — реестр' }], + [{ name: 'BT7', cond: '↔ Sentry трейс vs ошибки' }, { name: 'BT8', cond: '↔ Pail/Boost трейс vs tail/снапшот' }], + [{ name: 'Sentry', cond: 'трейс ↔ ошибки (ADR-013)' }] + ), + // ── СКИЛЫ SUPERPOWERS ──────────────────────────── sk_brainstorm: nd( 'Продумывает задачу вместе с заказчиком, формулирует варианты A/B/C и согласует дизайн до написания кода.', @@ -1807,6 +1841,12 @@ const NODE_META = { billing_audit: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел' }, ru_tax: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел' }, + // ── A1 BACKEND-TOOLING (20.05.2026, ADR-013) ── + rector: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел' }, + php_insights: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел' }, + backend_patterns: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел' }, + nightowl: { since: '20.05.2026', changed: '—', uses: 0, usesSrc: 'новый узел (DEFERRED)' }, + // ── BRAIN GOVERNANCE iter9 (19.05.2026, ADR-011) ── // uses: observer_stophook=31 эпизодов; lh_obs_obs/status_md/obs_cov=112 коммитов с 19.05 // (glob-less, каждый коммит); lh_l1watcher=10, lh_crossref=13 (коммиты по glob с 19.05); From d86d375ce42d730a3cba08dc3a0e11be8614923f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:42:41 +0300 Subject: [PATCH 02/11] docs(observer): chain attribution L1-L13 spec + plan + brain-retro #2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cspell-words.txt | 7 + docs/observer/STATUS.md | 6 +- .../notes/2026-05-20-brain-retro-v2.md | 219 +++++ .../2026-05-20-observer-chain-attribution.md | 820 ++++++++++++++++++ ...05-20-observer-chain-attribution-design.md | 274 ++++++ 5 files changed, 1323 insertions(+), 3 deletions(-) create mode 100644 docs/observer/notes/2026-05-20-brain-retro-v2.md create mode 100644 docs/superpowers/plans/2026-05-20-observer-chain-attribution.md create mode 100644 docs/superpowers/specs/2026-05-20-observer-chain-attribution-design.md diff --git a/cspell-words.txt b/cspell-words.txt index bcbfb07c..831b9a12 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1572,3 +1572,10 @@ lemed # Сквозной чек-лист портала + 6 фиксов (2026-05-21) захардкоженным смердженных + +# Observer chain attribution L1-L13 (2026-05-20) +инвокированный +межэпизодные +побочек +диффы +ретрофилл diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 2211587a..d9da907a 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-21T01:18:52.154Z +Last updated: 2026-05-21T01:29:26.077Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,11 +8,11 @@ Last updated: 2026-05-21T01:18:52.154Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ⚠️ | 16 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) | +| C5 Observer-coverage | ✅ | 37 episode(s) this month · Stop-hook + post-commit OK | ## Метрики (информационные, не алерты) -- Observer evidence: 16 episodes this month, 0 observer_error markers, 3 PII matches before filter +- Observer evidence: 37 episodes this month, 0 observer_error markers, 36 PII matches before filter - Legacy v1 episodes (not in factor analysis): 5 - Last /brain-retro: 2 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). diff --git a/docs/observer/notes/2026-05-20-brain-retro-v2.md b/docs/observer/notes/2026-05-20-brain-retro-v2.md new file mode 100644 index 00000000..dd2b1ff8 --- /dev/null +++ b/docs/observer/notes/2026-05-20-brain-retro-v2.md @@ -0,0 +1,219 @@ +# Brain-retro #2 — весь май 2026 (полный срез) + +**Дата:** 2026-05-20 (вечер, ~17:55 MSK) +**Период:** весь май 2026 — 2026-05-19T05:18Z .. 2026-05-20T08:58Z (28 строк JSONL; 23 v2-эпизода + 5 v1 пропущено). +**Источник:** `docs/observer/episodes-2026-05.jsonl` (28 строк) + `docs/observer/.read-counter.json`. +**Анализатор:** `node tools/brain-retro-analyzer.mjs docs/observer/episodes-2026-05.jsonl`. +**Отношение к предыдущему ретро:** надстройка над [2026-05-20-brain-retro.md](2026-05-20-brain-retro.md) (то — 17 v2-эпизодов, 12:25 MSK); здесь — те же 17 + дельта в 6 новых. +**Уровень анализа:** верхнеуровневый по запросу заказчика; экономия 100%. + +> Анализатор: `episodeCount=23`, `v1SkippedCount=5`, `observerErrorCount=0`. Все цифры по 23 v2-эпизодам, если не отмечено иное. + +--- + +## Period + +2026-05-19T05:18:16Z .. 2026-05-20T08:58:44Z. **7 уникальных task_id (сессий)**, 23 v2-анализируемых эпизода. + +Дельта vs прошлое ретро (6 новых эпизодов после 2026-05-20T08:12:29Z): + +| task_id | turn | start..end (Z) | path_type | provenance | node_chosen | econ | tool_calls | files | events примечательное | inferred outcome | +|---|---|---|---|---|---|---|---|---|---|---| +| `98298ec2` | 5 | 08:13..08:19 | improvised | autonomous | direct | 100 | 19 | 4 | 2× error tool_result + 2× retry | success (continuation) | +| `35fc31da` | 1 | 08:16..08:24 | improvised | autonomous | **brain-retro** | 0 | 20 | 6 | skill_invoked brain-retro | **rework** (следующий ход — correction) | +| `35fc31da` | 2 | 08:30..08:36 | improvised | autonomous | direct | 5 | 17 | 4 | 1× error + retry; prompt_signal=**correction** | success | +| `35fc31da` | 3 | 08:36..08:37 | improvised | user_directed_method (`claude_would_have_chosen=brain-retro`) | direct | null | 0 | 0 | — | unknown (no-op ход) | +| `35fc31da` | 4 | 08:39..08:46 | improvised | autonomous | direct | 0 | 15 | 14 | 14× Read mass | success | +| `286dd904` | 2 | 07:52..08:58 | **regulated** | **user_chose_from_options** ("На Plan 3 (экспорт)") | **superpowers:verification-before-completion** | 5 | 133 | 25 | skill_invoked verification; 1× error; **time_burn 66 мин** | unknown (нет следующего эпизода) | + +--- + +## Path-type distribution (v2, n=23) + +| path_type | count | % | +|---|---|---| +| improvised | 20 | 87.0% | +| regulated | 3 | 13.0% | +| alternative | 0 | 0% | +| mixed | 0 | 0% | + +Доля regulated на 4.6 п.п. ниже прошлого ретро (17.6% → 13.0%) — три новых improvised-эпизода без skill в дельте сдвинули долю. + +## Outcome distribution + +| outcome | count | % | +|---|---|---| +| success (inferred) | 9 | 39.1% | +| rework (inferred) | 1 | 4.3% | +| unknown (последние/нет следующего) | 13 | 56.5% | + +«Unknown» здесь — это эпизоды, после которых нет хода с positive/correction-сигналом (хвост сессий) — не provenance-bug. + +## Skill invocations (events `skill_invoked`, n=6) + +| skill | times | sessions | +|---|---|---| +| superpowers:verification-before-completion | 2 | `553717ec`, `286dd904` | +| superpowers:systematic-debugging | 1 | `553717ec` | +| superpowers:test-driven-development | 1 | `553717ec` | +| claude-md-management:claude-md-improver | 1 | `553717ec` | +| brain-retro | 1 | `35fc31da` | + +## Factor analysis matrix (analyzer `factorMatrix`) + +### decision_provenance — «rework мой или роутера?» + +| provenance | success | rework | unknown | +|---|---|---|---| +| autonomous | 6 | 1 | 10 | +| user_directed_method | 2 | 0 | 2 | +| user_chose_from_options | 0 | 0 | 2 | + +Единственный rework (`brain-retro` turn 1) — autonomous-выбор узла brain-retro заказчиком (это сам skill, инвокированный по `/brain-retro`); коррекция — про точность аналитики прошлого ретро, не про routing. **«Rework мой, не роутера.»** + +### economy_level + +| economy_level | success | rework | unknown | +|---|---|---|---| +| null | 3 | 0 | 2 | +| 0 | 0 | 1 | 2 | +| 5 | 4 | 0 | 6 | +| 100 | 1 | 0 | 4 | + +Слишком маленькая выборка для выводов; единственный rework на 0% — это brain-retro turn 1 (для самого ретро economy=0% это норма, заказчик так попросил). + +### model · post_compaction · task_size + +| factor value | success | rework | unknown | +|---|---|---|---| +| model: claude-opus-4-7 | 8 | 1 | 14 | +| post_compaction=true | 6 | 0 | 5 | +| post_compaction=false | 2 | 1 | 9 | +| session_turn late (≥10) | 6 | 0 | 5 | +| session_turn early (<10) | 2 | 1 | 9 | +| task_size small | 8 | 0 | 11 | +| task_size medium | 0 | 1 | 2 | +| task_size large | 0 | 0 | 1 | + +Все эпизоды на одной модели → строка про model — не сигнал. Post_compaction=true и late session_turn — это одна и та же длинная brain-governance сессия `553717ec` (turn 82+); концентрация success там — артефакт сессии, не закономерность. + +### node_chosen · task_classification + +| node_chosen | success | rework | unknown | +|---|---|---|---| +| direct | 8 | 0 | 11 | +| superpowers:verification-before-completion | 0 | 0 | 1 | +| superpowers:systematic-debugging | 0 | 0 | 1 | +| superpowers:test-driven-development | 0 | 0 | 1 | +| brain-retro | 0 | 1 | 0 | + +| task_classification | success | rework | unknown | +|---|---|---|---| +| bugfix | 2 | 0 | 1 | +| feature | 2 | 0 | 2 | +| other | 3 | 0 | 9 | +| refactor | 1 | 0 | 0 | +| question | 0 | 1 | 2 | + +«direct» — 8/0/11 — основная масса задач без skill-маршрутизации, всё работает. superpowers-узлы (3 эпизода, все unknown) сидят в хвостах своих сессий — нет следующего хода с явным signal. + +## Episodes → tasks (analyzer `tasks`, 15 task-групп) + +| task_ref | episodes | rework turns | +|---|---|---| +| `553717ec#1..#10` | 10 | turn 82 (rework — improvised CLAUDE.md edit, retry-recovered) | +| `24acfa10#1` | 1 | — | +| `a42e4ba5#1` | 1 | — | +| `dd905ea0#1` | 1 | — | +| `98298ec2#1..#3` | 3 (continuation across 5 turns) | — | +| `35fc31da#1..#4` | 4 | turn 1 (brain-retro) — correction в turn 2 | +| `286dd904#1` | 1 (66-min verification) | — | + +## Causal-chain candidates (analyzer `causalChains`) + +| from | to | shared files | +|---|---|---| +| — | — | — | + +Анализатор не нашёл «errored episode → fix episode на тех же файлах». 7 событий error (`tool_result reported is_error`) — это transient-сбои тулов внутри одного эпизода с retry-recovery, не межэпизодные. + +## Observer health + +- `observerErrorCount = 0` — за весь май **ни одного** `observer_error`-маркера. Парсер ни разу не сломался тихо. +- `interrupts = 0` — заказчик ни разу не прерывал ход. +- `errors = 7` (внутри 5 эпизодов) — все transient, retry-recovered. +- `retries = 6` — корреспондируют ошибкам один-в-один (один retry бесплатный после restart-tooling). +- `time_burn_total = 86 мин` — из них 66 мин — один эпизод `286dd904#2` (длинная verification-сессия Plan 2 экспорт). + +## Canonical chains L1–L12 hit rate + +Не считаем за май — нет атрибуции `chain_ref`; routing-таблица `docs/routing-off-phase.md` v1.2 ещё не интегрирована в primary_rationale. Кандидат на доработку парсера — см. ниже. + +## Improvised chains (повторённые ≥2) + +| node-set | times | candidate L13+? | +|---|---|---| +| direct → direct (continuation в одной сессии) | 14 | нет — это норма, не цепочка | + +Других повторов нет. + +## chain_divergence cases + +Нет атрибуции — пропуск. + +## Top error classes + +| error class | count | recovery pattern | +|---|---|---| +| `tool_result reported is_error` (transient) | 7 | retry в том же эпизоде, без user-intervention | + +## confusion_marker hot-spots + +Нет таких маркеров в схеме v2 — пропуск. + +--- + +## Candidates for owner review + +### Candidate 1: уточнить analyzer для дельта-сравнения с предыдущим ретро + +- **Type:** doc/skill enhancement, не нормативная правка. +- **Evidence:** прошлое ретро (35fc31da#1) → `prompt_signal=correction` следующего хода (35fc31da#2). Анализатор корректно пометил `outcome=rework`, но в выводе нет указателя на номер прошлого ретро или diff vs предыдущий. +- **Suggested action:** в `tools/brain-retro-analyzer.mjs` добавить опциональный аргумент `--since ` (срез по `started_at >= since`), чтобы можно было дёшево считать только дельту между ретро. Альтернатива: в шаблоне `.claude/skills/brain-retro/references/aggregation-template.md` добавить секцию «Delta vs prior retro» с явным diff'ом. +- **Cost / risk:** низкий; чистый node-скрипт без побочек на JSONL. Сейчас процесс ручной (этот ретро diff делался руками). +- **Rejection option:** заказчик может сказать «всегда срез — полный месяц», и тогда диффы не нужны. + +### Candidate 2: атрибуция canonical chains L1–L12 в primary_rationale + +- **Type:** observer schema extension (потребует amend ADR-011 / spec factor-analysis). +- **Evidence:** ни один из 23 эпизодов не несёт ссылки на L1–L12 chain из `docs/routing-off-phase.md` v1.2 (а с finance-tooling — там уже L1–L13). «Canonical chains hit rate» — пустая таблица. +- **Suggested action:** в `tools/observer-routing-detector.mjs` (или новый детектор) маппить выбранные узлы в L-цепочку и писать `primary_rationale.chain_ref: "L7"` (например). Только тогда можно отслеживать чистоту следования цепочкам. +- **Cost / risk:** средний — нужен маппинг «node_chosen → L-chain», который сейчас живёт только в человеческом тексте routing-off-phase.md. Риск: дрейф маппинга между парсером и документом. +- **Rejection option:** оставить L1–L13 как нормативное «чтение для человека», не пытаться формализовать. + +### Candidate 3: проверить корректность аналитики прошлого ретро + +- **Type:** ad-hoc review (не нормативка). +- **Evidence:** rework-flag на 35fc31da#1 — единственный rework в выборке. Я (текущее ретро) дельту посчитал и нашёл, что предыдущее ретро отчиталось «17 эпизодов, 5 v1 пропущено» — это совпадает с записанным; коррекция была про что-то другое (содержание ретро, не структура). Без доступа к самой формулировке коррекции (`35fc31da#2` body) можно только сказать: коррекция структурно не была про observability, а про текст ретро. +- **Suggested action:** заказчик при желании может прокомментировать «что именно правил после ретро 12:25», и записать урок в notes. **Не блокирующее.** +- **Rejection option:** игнорировать — мелкая коррекция текста, не системный сигнал. + +(Других кандидатов нет. Никаких removals/zombie nodes per memory `feedback_brain_unused_tools_not_problem`.) + +--- + +## Informational metrics (NOT alerts) + +- Узлов, использованных хотя бы раз за период (явно через `skill_invoked`): **5 / 63+** (superpowers TDD/debug/verify, claude-md-improver, brain-retro). Узел `direct` (=прямое исполнение) — отдельная категория, 19 эпизодов. +- Узлов, ни разу не использованных с начала наблюдения: **большинство (≥55 из 63+)** — **не проблема** per [feedback_brain_unused_tools_not_problem](../../../../C:/Users/Administrator/.claude/projects/c---------------------crm-------------/memory/feedback_brain_unused_tools_not_problem.md). Capability-readiness — осознанная стратегия заказчика. +- Параллельные сессии: 12 эпизодов из 23 (52%) с `parallel_session=true` — норма для текущего рабочего режима с §15 Pravila. +- Длинные эпизоды (`time_burn` событие): 1 — `286dd904#2` 66 мин (Plan 2 экспорт verification). Все остальные — без time_burn-маркера (под 60 мин). + +--- + +## Дельта vs прошлое ретро 2026-05-20 12:25 — итог + +- Картина устойчива: improvised-доминанта, opus-4-7-only, 0 observer_error, rework на autonomous-выборах единичный. +- Новый класс события: **`prompt_signal=correction` после skill-инвокации** (35fc31da#1 → #2). Раньше correction наблюдался только на autonomous direct-выборах; теперь видно, что brain-retro skill тоже не неприкосновенен — это здоровый сигнал. +- Новый успешный кейс гибридной модели: **`user_chose_from_options` → regulated path** (286dd904#2, 66 мин, 133 tool_calls, через superpowers:verification-before-completion). Это первое подтверждение, что collaborative-choice + skill-маршрутизация даёт длинные продуктивные эпизоды без interruptions. +- Никаких рекомендаций править Pravila / PSR_v1 / Tooling / CLAUDE.md — выборка (23 эпизода / 2 дня) слишком мала. diff --git a/docs/superpowers/plans/2026-05-20-observer-chain-attribution.md b/docs/superpowers/plans/2026-05-20-observer-chain-attribution.md new file mode 100644 index 00000000..6a613ad4 --- /dev/null +++ b/docs/superpowers/plans/2026-05-20-observer-chain-attribution.md @@ -0,0 +1,820 @@ +# Observer Canonical Chain Attribution (L1–L13) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Добавить опциональное поле `chain_ref` в эпизоды наблюдателя, связывающее `node_chosen` с каноническими цепочками L1–L13, чтобы `/brain-retro` мог считать «hit rate цепочек». + +**Architecture:** Один новый слой над наблюдателем — статический JSON-маппинг `node_chosen → [LN]`, чистая функция-детектор, врезка в парсер транскрипта, контролёр C6 сверки JSON↔`routing-off-phase.md` (в lefthook pre-commit + Vitest), одноразовый ретрофилл существующих эпизодов, агрегация в анализаторе. Всё детерминированно — 0 LLM-вызовов. + +**Tech Stack:** Node 20+ ESM, Vitest 4.1.5, pure fs/regex (Security Guidance #40 — никаких shell-вызовов в parser/hook). Раннер: `npm run test:tools` (`cd app && npx vitest run --config vitest.config.tools.mjs`). Тесты лежат рядом с модулями: `tools/.test.mjs`. + +**Spec:** [docs/superpowers/specs/2026-05-20-observer-chain-attribution-design.md](../specs/2026-05-20-observer-chain-attribution-design.md). + +--- + +## ⚠️ Внешняя зависимость и порядок (читать ПЕРВЫМ) + +**Этот план НЕ исполняется немедленно.** Жёсткая зависимость от epic-плана [2026-05-20-observer-instrument-expansion.md](2026-05-20-observer-instrument-expansion.md) (20 task), который правит тот же `tools/observer-transcript-parser.mjs`. + +**Процедура старта (Task 0 ниже):** + +1. Дождаться сообщения «epic 20-task закрыт и push'нут на origin/main». +2. `git fetch origin && git log HEAD..origin/main --oneline` — убедиться, что 20 task влиты (Pravila §15.2 pre-flight). +3. Создать свежий worktree off `origin/main` (Task 0). +4. Исполнять Tasks 1–9 по порядку. + +--- + +## Уточнения к spec (раскрыты при детализации writing-plans) + +1. **Расположение тестов.** Spec §5/§8 называл `tests/observer-chain-*.test.mjs`. Реальный паттерн репозитория — тест рядом с модулем: `tools/observer-chain-detector.test.mjs`. План использует реальный паттерн. + +2. **Семантика контролёра C6 — по L-номерам, не по именам узлов.** Имена в таблице `routing-off-phase.md` — человеческие display-names (`Boost MCP`, `Trail of Bits`, `Semgrep MCP`), а `node_chosen` в эпизодах — технические skill-id (`superpowers:test-driven-development`, `claude-md-management:claude-md-improver`, `direct`). Прямая построчная сверка имён хрупкая. Поэтому C6 v1 сверяет **множества L-номеров**: + - каждый `LN`, упомянутый в JSON, существует в `.md` (нет ссылок на несуществующую цепочку); + - каждый `LN` из таблицы `.md` присутствует хотя бы в одной записи JSON (цепочка не «потеряна» при добавлении новой L в .md). + + Это ловит главный класс дрейфа («добавили L14 в .md — JSON про неё не знает»). Точечная сверка «узел X в L7» через display-name-алиасы — out of scope v1 (можно как future-слой). + +3. **Точка врезки в parser.** На `origin/main` это `return { … node_chosen: skills.length > 0 ? skills[0] : 'direct', … }` (≈строка 696). После epic-плана номер строки сдвинется — **искать по grep-маркеру** `node_chosen: skills.length > 0 ? skills[0] : 'direct'`, не по номеру. + +--- + +## File Structure + +| Файл | Тип | Ответственность | +|---|---|---| +| `tools/observer-chain-map.json` | новый (data) | Маппинг `node_chosen` (реальное значение) → массив `["LN"]`. Только узлы, входящие в L1–L13 | +| `tools/observer-chain-detector.mjs` | новый | `loadChainMap(path)` + чистая `chainsFor(node, map)` → массив \| `null` | +| `tools/observer-chain-detector.test.mjs` | новый | Юнит-тесты `chainsFor` | +| `tools/observer-transcript-parser.mjs` | edit | Врезка `chain_ref` в `primary_rationale` | +| `tools/observer-transcript-parser.test.mjs` | edit | +тест что эпизод несёт `chain_ref` | +| `tools/observer-chain-map-checker.mjs` | новый | C6: `parseChainsFromMd()` + `checkSync()` + CLI | +| `tools/observer-chain-map-checker.test.mjs` | новый | Тесты парсера .md + sync-сверки | +| `lefthook.yml` | edit | Job 16 `observer-chain-map-checker` в pre-commit | +| `tools/observer-retrofill-chain-ref.mjs` | новый | Одноразовый ретрофилл `chain_ref` в JSONL | +| `tools/observer-retrofill-chain-ref.test.mjs` | новый | Тесты идемпотентности + dry-run | +| `tools/brain-retro-analyzer.mjs` | edit | `factorMatrix.chain_ref` + `chainHitRate` | +| `tools/brain-retro-analyzer.test.mjs` | edit | +тест агрегации chain_ref | +| `tools/status-md-generator.mjs` | edit | Строка «C6 Chain map sync» | +| `tools/status-md-generator.test.mjs` | edit | +тест строки C6 | +| `.claude/skills/brain-retro/references/aggregation-template.md` | edit | Заполнить секцию «L1–L13 hit rate» | + +--- + +## Task 0: Pre-flight + worktree (организационный, не код) + +**Files:** нет правок кода. + +- [ ] **Step 1: Убедиться, что epic-план влит** + +Run: + +```bash +git fetch origin +git log --oneline -5 origin/main +git log HEAD..origin/main --oneline | grep -i "observer-instrument-expansion\|Task 21\|Task 20" || echo "epic не найден — НЕ СТАРТОВАТЬ" +``` + +Expected: видны коммиты закрытия epic 20-task на origin/main. Если нет — остановиться, сообщить владельцу. + +- [ ] **Step 2: Создать worktree off origin/main** + +Использовать `superpowers:using-git-worktrees`. Целевая база — `origin/main` (свежий, после epic). Ветка `feat/observer-chain-attribution`. + +- [ ] **Step 3: Verify базовая регрессия зелёная** + +Run: `npm run test:tools` +Expected: PASS (≥ baseline после epic, например 350+/350+). Записать число baseline для финальной сверки. + +--- + +## Task 1: chain-map JSON + детектор `chainsFor` + +**Files:** + +- Create: `tools/observer-chain-map.json` +- Create: `tools/observer-chain-detector.mjs` +- Test: `tools/observer-chain-detector.test.mjs` + +- [ ] **Step 1: Создать JSON-маппинг** + +`tools/observer-chain-map.json` — только узлы, входящие в L1–L13. Имена ключей = реальные значения `node_chosen` (skill-id). NB: значения `node_chosen` берутся из первого `skill_invoked` (`skills[0]`); для skill-узлов это `plugin:skill` или `skill`. MCP-узлы (Boost/Sentry/Redis) в `node_chosen` не появляются (они не skill_invoked) — но включены в маппинг на будущее, если детектор узлов расширится. + +```json +{ + "_note": "node_chosen -> L-цепочки. Только узлы, входящие хотя бы в одну L1-L13. Узлы вне цепочек (direct, прочее) НЕ включаются -> chainsFor вернёт null. Имена ключей = реальные значения primary_rationale.node_chosen. Синхронизируется с docs/routing-off-phase.md через контролёр C6 (tools/observer-chain-map-checker.mjs).", + "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:adr": ["L4", "L5"], + "adr-kit:judge": ["L5"], + "operations": ["L4"], + "architecture-patterns:architecture-patterns": ["L5"], + "deptrac": ["L5"], + "security-review": ["L6"], + "superpowers:systematic-debugging": ["L8"], + "ccpm": ["L9"], + "product-management:brainstorm": ["L9"], + "promptfoo": ["L10"], + "data-scientist": ["L10"], + "claude-api": ["L10"], + "skill-creator:skill-creator": ["L11"], + "hookify:hookify": ["L11"], + "plugin-dev:create-plugin": ["L11"], + "claude-md-management:claude-md-improver": ["L12"], + "claude-md-management:revise-claude-md": ["L12"], + "billing-audit": ["L13"], + "ru-tax-accounting": ["L13"] +} +``` + +- [ ] **Step 2: Написать failing-тест детектора** + +`tools/observer-chain-detector.test.mjs`: + +```javascript +import { describe, it, expect } from 'vitest'; +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; + +const map = loadChainMap(); + +describe('chainsFor', () => { + it('returns chain array for a single-chain node', () => { + expect(chainsFor('billing-audit', map)).toEqual(['L13']); + }); + + it('returns all chains for a multi-chain node', () => { + expect(chainsFor('discovery-interview', map)).toEqual(['L1', 'L2']); + }); + + it('returns null for direct', () => { + expect(chainsFor('direct', map)).toBeNull(); + }); + + it('returns null for an unknown node', () => { + expect(chainsFor('totally-unknown-xyz', map)).toBeNull(); + }); + + it('returns null for empty/null/undefined', () => { + expect(chainsFor('', map)).toBeNull(); + expect(chainsFor(null, map)).toBeNull(); + expect(chainsFor(undefined, map)).toBeNull(); + }); + + it('ignores the _note metadata key', () => { + expect(chainsFor('_note', map)).toBeNull(); + }); +}); +``` + +- [ ] **Step 3: Запустить тест — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-chain-detector.test.mjs` +Expected: FAIL — `loadChainMap is not a function` / module not found. + +- [ ] **Step 4: Реализовать детектор** + +`tools/observer-chain-detector.mjs`: + +```javascript +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_MAP_PATH = join(__dirname, 'observer-chain-map.json'); + +/** Load the node->chains map. Throws on missing/invalid JSON (caller handles). */ +export function loadChainMap(path = DEFAULT_MAP_PATH) { + const raw = JSON.parse(readFileSync(path, 'utf8')); + const map = new Map(); + for (const [node, chains] of Object.entries(raw)) { + if (node === '_note') continue; + if (Array.isArray(chains) && chains.length > 0) map.set(node, chains); + } + return map; +} + +/** node_chosen -> array of L-chains, or null if not in any chain. */ +export function chainsFor(node, map) { + if (!node || typeof node !== 'string') return null; + const chains = map.get(node); + return chains && chains.length > 0 ? chains : null; +} +``` + +- [ ] **Step 5: Запустить тест — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-chain-detector.test.mjs` +Expected: PASS (6 tests). + +- [ ] **Step 6: Commit** + +```bash +git add tools/observer-chain-map.json tools/observer-chain-detector.mjs tools/observer-chain-detector.test.mjs +git commit -m "feat(observer): chain-map JSON + chainsFor detector (L1-L13 attribution)" +``` + +--- + +## Task 2: Врезка `chain_ref` в парсер транскрипта + +**Files:** + +- Modify: `tools/observer-transcript-parser.mjs` (grep-маркер `node_chosen: skills.length > 0 ? skills[0] : 'direct'`) +- Test: `tools/observer-transcript-parser.test.mjs` + +- [ ] **Step 1: Написать failing-тест** + +Добавить в `tools/observer-transcript-parser.test.mjs` (в подходящий describe для primary_rationale; адаптировать фабрику транскрипта под существующие хелперы файла): + +```javascript +it('attaches chain_ref for a node that belongs to a chain', () => { + // транскрипт с skill_invoked = 'billing-audit' (адаптировать под фабрику файла) + const episode = parseTranscript(transcriptWithSkill('billing-audit')); + expect(episode.primary_rationale.chain_ref).toEqual(['L13']); +}); + +it('sets chain_ref null for a direct episode', () => { + const episode = parseTranscript(transcriptWithNoSkill()); + expect(episode.primary_rationale.chain_ref).toBeNull(); +}); +``` + +NB: точные имена хелперов (`parseTranscript` / фабрики) взять из существующего теста — не выдумывать. Если фабрики нет — собрать минимальный transcript-объект вручную по образцу соседних тестов. + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-transcript-parser.test.mjs -t chain_ref` +Expected: FAIL — `chain_ref` undefined. + +- [ ] **Step 3: Реализовать врезку** + +В `tools/observer-transcript-parser.mjs`: + +1. Вверху файла добавить импорт: + +```javascript +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; +``` + +1. Один раз модульно загрузить карту с защитой от битого JSON: + +```javascript +let CHAIN_MAP = null; +try { + CHAIN_MAP = loadChainMap(); +} catch { + CHAIN_MAP = new Map(); // битый/отсутствующий JSON -> chainsFor вернёт null, observer не падает +} +``` + +1. Найти grep-маркер `node_chosen: skills.length > 0 ? skills[0] : 'direct'` и добавить рядом строку. Должно получиться: + +```javascript + node_chosen: skills.length > 0 ? skills[0] : 'direct', + chain_ref: chainsFor(skills.length > 0 ? skills[0] : 'direct', CHAIN_MAP), +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-transcript-parser.test.mjs` +Expected: PASS (включая существующие тесты файла — ни один не сломан). + +- [ ] **Step 5: Commit** + +```bash +git add tools/observer-transcript-parser.mjs tools/observer-transcript-parser.test.mjs +git commit -m "feat(observer): emit chain_ref in primary_rationale" +``` + +--- + +## Task 3: Контролёр C6 — sync JSON ↔ routing-off-phase.md + +**Files:** + +- Create: `tools/observer-chain-map-checker.mjs` +- Test: `tools/observer-chain-map-checker.test.mjs` + +- [ ] **Step 1: Написать failing-тест** + +`tools/observer-chain-map-checker.test.mjs`: + +```javascript +import { describe, it, expect } from 'vitest'; +import { parseChainsFromMd, checkSync } from './observer-chain-map-checker.mjs'; + +const SAMPLE_MD = [ + '| # | Цепочка | Зачем |', + '|---|---|---|', + '| L1 | `discovery-interview` (FEATURE) → `brainstorming` | text |', + '| L2 | `audit-portal` | text |', + '| L13 | `billing-audit` (#62) + `Pest` | text |', +].join('\n'); + +describe('parseChainsFromMd', () => { + it('extracts the set of L-numbers from the table', () => { + expect(parseChainsFromMd(SAMPLE_MD)).toEqual(new Set(['L1', 'L2', 'L13'])); + }); +}); + +describe('checkSync', () => { + it('passes when JSON L-numbers subset of md and md subset of json-union', () => { + const mdSet = new Set(['L1', 'L2', 'L13']); + const jsonMap = { a: ['L1'], b: ['L2'], c: ['L13'] }; + expect(checkSync(jsonMap, mdSet).ok).toBe(true); + }); + + it('fails when JSON references a chain absent from md', () => { + const mdSet = new Set(['L1', 'L2']); + const jsonMap = { a: ['L1'], b: ['L99'] }; + const res = checkSync(jsonMap, mdSet); + expect(res.ok).toBe(false); + expect(res.jsonOnly).toContain('L99'); + }); + + it('fails when md has a chain not covered by any JSON entry', () => { + const mdSet = new Set(['L1', 'L2', 'L14']); + const jsonMap = { a: ['L1'], b: ['L2'] }; + const res = checkSync(jsonMap, mdSet); + expect(res.ok).toBe(false); + expect(res.mdOnly).toContain('L14'); + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-chain-map-checker.test.mjs` +Expected: FAIL — module not found. + +- [ ] **Step 3: Реализовать чекер** + +`tools/observer-chain-map-checker.mjs`: + +```javascript +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const MD_PATH = join(__dirname, '..', 'docs', 'routing-off-phase.md'); +const JSON_PATH = join(__dirname, 'observer-chain-map.json'); + +/** Extract the set of L-numbers ("L1".."L13") from the routing-off-phase.md table. */ +export function parseChainsFromMd(md) { + const set = new Set(); + for (const line of md.split(/\r?\n/)) { + const m = /^\|\s*(L\d+)\s*\|/.exec(line.trim()); + if (m) set.add(m[1]); + } + return set; +} + +/** Compare JSON L-numbers against the md set, both directions. */ +export function checkSync(jsonMap, mdSet) { + const jsonSet = new Set(); + for (const [node, chains] of Object.entries(jsonMap)) { + if (node === '_note') continue; + if (Array.isArray(chains)) for (const c of chains) jsonSet.add(c); + } + const jsonOnly = [...jsonSet].filter((c) => !mdSet.has(c)); // ссылки на несуществующие L + const mdOnly = [...mdSet].filter((c) => !jsonSet.has(c)); // потерянные цепочки + return { ok: jsonOnly.length === 0 && mdOnly.length === 0, jsonOnly, mdOnly }; +} + +/** CLI entry — exit 1 on drift with a human-readable message. */ +function main() { + const md = readFileSync(MD_PATH, 'utf8'); + const jsonMap = JSON.parse(readFileSync(JSON_PATH, 'utf8')); + const mdSet = parseChainsFromMd(md); + if (mdSet.size === 0) { + console.error('[chain-map-checker] не нашёл ни одной L-строки в routing-off-phase.md — формат таблицы изменился?'); + process.exit(1); + } + const res = checkSync(jsonMap, mdSet); + if (res.ok) { + console.log(`[chain-map-checker] OK — ${mdSet.size} chains in sync`); + process.exit(0); + } + console.error('[chain-map-checker] дрейф маппинга chain-map <-> routing-off-phase.md:'); + if (res.jsonOnly.length) console.error(` JSON ссылается на отсутствующие в .md цепочки: ${res.jsonOnly.join(', ')}`); + if (res.mdOnly.length) console.error(` В .md есть цепочки без записи в JSON: ${res.mdOnly.join(', ')} — добавьте узлы в tools/observer-chain-map.json`); + process.exit(1); +} + +if (process.argv[1]?.endsWith('observer-chain-map-checker.mjs')) { + main(); +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-chain-map-checker.test.mjs` +Expected: PASS (4 tests). + +- [ ] **Step 5: Smoke CLI на реальных файлах** + +Run: `node tools/observer-chain-map-checker.mjs` +Expected: `[chain-map-checker] OK — 13 chains in sync` (exit 0). Если FAIL — привести JSON (Task 1) в соответствие с реальной таблицей `.md` (это раскроет реальные расхождения первого маппинга). + +- [ ] **Step 6: Commit** + +```bash +git add tools/observer-chain-map-checker.mjs tools/observer-chain-map-checker.test.mjs +git commit -m "feat(observer): C6 chain-map-checker (JSON vs routing-off-phase.md sync)" +``` + +--- + +## Task 4: lefthook job 16 + red-green smoke + +**Files:** + +- Modify: `lefthook.yml` (после job 15 observer-coverage-checker) + +- [ ] **Step 1: Добавить job** + +В `lefthook.yml`, в секцию pre-commit после job 15: + +```yaml + # 16. observer-chain-map-checker — brain governance C6 (chain attribution). + # Сверяет tools/observer-chain-map.json с таблицей L1-L13 в + # docs/routing-off-phase.md. Падает при дрейфе (несуществующая L в JSON + # или потерянная цепочка из .md). + - name: observer-chain-map-checker + run: node tools/observer-chain-map-checker.mjs + fail_text: | + observer-chain-map-checker: дрейф chain-map <-> routing-off-phase.md. + Обновите tools/observer-chain-map.json под таблицу L1-L13. +``` + +NB: точный синтаксис (`fail_text` vs `interactive` vs `|| true`) скопировать с соседних observer-job'ов (11–15) — формат должен совпасть. + +- [ ] **Step 2: Red-green smoke** + +Run (намеренная рассинхронизация): + +```bash +# временно добавить несуществующую цепочку в JSON +node -e "const f='tools/observer-chain-map.json';const fs=require('fs');const j=JSON.parse(fs.readFileSync(f,'utf8'));j['__test_drift__']=['L99'];fs.writeFileSync(f,JSON.stringify(j,null,2));" +node tools/observer-chain-map-checker.mjs; echo "exit=$?" +``` + +Expected: exit=1, сообщение про `L99`. + +```bash +# откатить +git checkout tools/observer-chain-map.json +node tools/observer-chain-map-checker.mjs; echo "exit=$?" +``` + +Expected: exit=0, `OK — 13 chains in sync`. + +- [ ] **Step 3: Commit** + +```bash +git add lefthook.yml +git commit -m "chore(lefthook): wire C6 observer-chain-map-checker (job 16)" +``` + +--- + +## Task 5: Ретрофилл существующих эпизодов + +**Files:** + +- Create: `tools/observer-retrofill-chain-ref.mjs` +- Test: `tools/observer-retrofill-chain-ref.test.mjs` + +- [ ] **Step 1: Написать failing-тест** + +`tools/observer-retrofill-chain-ref.test.mjs`: + +```javascript +import { describe, it, expect } from 'vitest'; +import { retrofillLine } from './observer-retrofill-chain-ref.mjs'; +import { loadChainMap } from './observer-chain-detector.mjs'; + +const map = loadChainMap(); + +describe('retrofillLine', () => { + it('adds chain_ref to a v2 episode with a known node', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'billing-audit' } }; + const out = retrofillLine(ep, map); + expect(out.primary_rationale.chain_ref).toEqual(['L13']); + }); + + it('sets chain_ref null for a direct v2 episode', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct' } }; + expect(retrofillLine(ep, map).primary_rationale.chain_ref).toBeNull(); + }); + + it('is idempotent — does not overwrite existing chain_ref', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct', chain_ref: ['L1'] } }; + expect(retrofillLine(ep, map).primary_rationale.chain_ref).toEqual(['L1']); + }); + + it('skips v1 episodes (no schema_version 2)', () => { + const ep = { foo: 'bar' }; + expect(retrofillLine(ep, map)).toEqual({ foo: 'bar' }); + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-retrofill-chain-ref.test.mjs` +Expected: FAIL — module not found. + +- [ ] **Step 3: Реализовать** + +`tools/observer-retrofill-chain-ref.mjs`: + +```javascript +import { readFileSync, writeFileSync, renameSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OBS_DIR = join(__dirname, '..', 'docs', 'observer'); + +/** Add chain_ref to a single parsed episode object (pure). Idempotent. */ +export function retrofillLine(ep, map) { + if (!ep || ep.schema_version !== 2 || !ep.primary_rationale) return ep; + if ('chain_ref' in ep.primary_rationale) return ep; // idempotent + ep.primary_rationale.chain_ref = chainsFor(ep.primary_rationale.node_chosen, map); + return ep; +} + +/** Process one JSONL file atomically (tmp + rename). Returns {changed, total}. */ +export function retrofillFile(path, map, { dryRun = false } = {}) { + const lines = readFileSync(path, 'utf8').split(/\r?\n/); + let changed = 0, total = 0; + const out = lines.map((line) => { + if (!line.trim()) return line; + total++; + const ep = JSON.parse(line); + const before = ep.primary_rationale && 'chain_ref' in ep.primary_rationale; + const next = retrofillLine(ep, map); + const after = next.primary_rationale && 'chain_ref' in next.primary_rationale; + if (!before && after) changed++; + return JSON.stringify(next); + }); + if (!dryRun && changed > 0) { + const tmp = `${path}.tmp`; + writeFileSync(tmp, out.join('\n'), 'utf8'); + renameSync(tmp, path); + } + return { changed, total }; +} + +function main() { + const dryRun = process.argv.includes('--dry-run'); + const map = loadChainMap(); + const files = readdirSync(OBS_DIR).filter((f) => /^episodes-\d{4}-\d{2}\.jsonl$/.test(f)); + for (const f of files) { + const { changed, total } = retrofillFile(join(OBS_DIR, f), map, { dryRun }); + console.log(`${dryRun ? '[dry-run] ' : ''}${f}: ${changed}/${total} lines would get chain_ref`); + } +} + +if (process.argv[1]?.endsWith('observer-retrofill-chain-ref.mjs')) main(); +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/observer-retrofill-chain-ref.test.mjs` +Expected: PASS (4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add tools/observer-retrofill-chain-ref.mjs tools/observer-retrofill-chain-ref.test.mjs +git commit -m "feat(observer): one-shot chain_ref retrofill script (idempotent, atomic)" +``` + +--- + +## Task 6: Агрегация в brain-retro-analyzer + +**Files:** + +- Modify: `tools/brain-retro-analyzer.mjs` (в формирование `factorMatrix`) +- Test: `tools/brain-retro-analyzer.test.mjs` + +- [ ] **Step 1: Написать failing-тест** + +Добавить в `tools/brain-retro-analyzer.test.mjs`: + +```javascript +it('aggregates chain_ref into factorMatrix (multi-chain counted in each)', () => { + const episodes = [ + { schema_version: 2, primary_rationale: { node_chosen: 'discovery-interview', chain_ref: ['L1','L2'] } /* + поля, нужные analyzer */ }, + { schema_version: 2, primary_rationale: { node_chosen: 'direct', chain_ref: null } }, + ]; + const result = analyze(episodes); // имя функции взять из существующего теста + expect(result.factorMatrix.chain_ref.L1).toBeDefined(); + expect(result.factorMatrix.chain_ref.L2).toBeDefined(); + expect(result.factorMatrix.chain_ref.null).toBeDefined(); +}); +``` + +NB: имена `analyze` / shape входа подогнать под существующий тест файла (там уже есть фикстуры эпизодов — переиспользовать форму). + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/brain-retro-analyzer.test.mjs -t chain_ref` +Expected: FAIL — `factorMatrix.chain_ref` undefined. + +- [ ] **Step 3: Реализовать** + +В `tools/brain-retro-analyzer.mjs`, где строится `factorMatrix`, добавить ось `chain_ref`. Multi-chain эпизод инкрементит каждую L; `null` → ключ `"null"`: + +```javascript +// внутри построения factorMatrix, по аналогии с другими осями: +matrix.chain_ref = {}; +for (const ep of v2Episodes) { + const cr = ep.primary_rationale?.chain_ref; + const outcome = ep._inferredOutcome ?? 'unknown'; + const keys = Array.isArray(cr) && cr.length ? cr : ['null']; + for (const k of keys) { + matrix.chain_ref[k] = matrix.chain_ref[k] || {}; + matrix.chain_ref[k][outcome] = (matrix.chain_ref[k][outcome] || 0) + 1; + } +} +``` + +NB: точные имена переменных (`matrix`, `v2Episodes`, поле inferred outcome) взять из реального кода — он уже строит другие оси, скопировать паттерн. + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/brain-retro-analyzer.test.mjs` +Expected: PASS (включая существующие тесты). + +- [ ] **Step 5: Commit** + +```bash +git add tools/brain-retro-analyzer.mjs tools/brain-retro-analyzer.test.mjs +git commit -m "feat(brain-retro): aggregate chain_ref into factorMatrix" +``` + +--- + +## Task 7: STATUS.md строка C6 + +**Files:** + +- Modify: `tools/status-md-generator.mjs` +- Test: `tools/status-md-generator.test.mjs` + +- [ ] **Step 1: Написать failing-тест** + +Добавить в `tools/status-md-generator.test.mjs`: + +```javascript +it('includes a C6 chain-map row', () => { + const md = generateStatus(/* фикстура как в существующих тестах */); + expect(md).toMatch(/C6 Chain map sync/); +}); +``` + +NB: имя `generateStatus` и форму входа взять из существующего теста. + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/status-md-generator.test.mjs -t C6` +Expected: FAIL. + +- [ ] **Step 3: Реализовать** + +В `tools/status-md-generator.mjs`, в таблицу контролёров добавить строку C6 (по аналогии с C5). Поскольку чекер запускается в lefthook, статус в STATUS.md — информационный: вызвать `checkSync` через импорт и отразить ok/drift: + +```javascript +import { parseChainsFromMd, checkSync } from './observer-chain-map-checker.mjs'; +// ... при сборке таблицы контролёров: +let c6 = '✅'; +let c6detail = '[chain-map-checker] OK'; +try { + const md = readFileSync(join(OBS_ROOT, '..', 'routing-off-phase.md'), 'utf8'); // путь привести к реальному + const jsonMap = JSON.parse(readFileSync(CHAIN_MAP_PATH, 'utf8')); + const res = checkSync(jsonMap, parseChainsFromMd(md)); + if (!res.ok) { c6 = '🔴'; c6detail = `drift: ${[...res.jsonOnly, ...res.mdOnly].join(', ')}`; } +} catch (e) { c6 = '⚠️'; c6detail = `checker error: ${e.message}`; } +// добавить строку в таблицу: | C6 Chain map sync | ${c6} | ${c6detail} | +``` + +NB: пути (`OBS_ROOT`, `CHAIN_MAP_PATH`) и формат строки таблицы взять из реального кода генератора. + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Run: `cd app && npx vitest run --config vitest.config.tools.mjs ../tools/status-md-generator.test.mjs` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add tools/status-md-generator.mjs tools/status-md-generator.test.mjs +git commit -m "feat(status-md): surface C6 chain-map sync row" +``` + +--- + +## Task 8: aggregation-template секция hit rate + +**Files:** + +- Modify: `.claude/skills/brain-retro/references/aggregation-template.md` + +- [ ] **Step 1: Заменить пустую секцию** + +Найти `## Canonical chains L1–L12 hit rate` и заменить на: + +```markdown +## Canonical chains L1–L13 hit rate (from analyzer `factorMatrix.chain_ref`) + +| chain | times | outcome split | notes | +|---|---|---|---| + +Каждый узел может входить в несколько L (multi-chain эпизод засчитан в каждую). +`null` = эпизоды вне цепочек (direct + узлы вне L1-L13) — **не проблема** per +`memory/feedback_brain_unused_tools_not_problem`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add .claude/skills/brain-retro/references/aggregation-template.md +git commit -m "docs(brain-retro): fill L1-L13 hit rate template section" +``` + +--- + +## Task 9: Финальная регрессия + ретрофилл + verification + +**Files:** нет правок кода. + +- [ ] **Step 1: Полная регрессия tools** + +Run: `npm run test:tools` +Expected: PASS, число ≥ baseline (Task 0 Step 3) + новые тесты (≈ +16). 0 сломанных существующих. + +- [ ] **Step 2: Ретрофилл dry-run** + +Run: `node tools/observer-retrofill-chain-ref.mjs --dry-run` +Expected: для `episodes-2026-05.jsonl` — N/M строк получат chain_ref (N = число v2-эпизодов с known node). + +- [ ] **Step 3: Ретрофилл реальный + идемпотентность** + +Run: + +```bash +node tools/observer-retrofill-chain-ref.mjs +node tools/observer-retrofill-chain-ref.mjs +``` + +Expected: первый — `changed > 0`; второй — `0/M` (идемпотентно). + +- [ ] **Step 4: Commit ретрофилла данных** + +```bash +git add docs/observer/episodes-2026-05.jsonl +git commit -m "chore(observer): retrofill chain_ref on existing May episodes" +``` + +- [ ] **Step 5: Verification-before-completion** + +Использовать `superpowers:verification-before-completion`. Проверить acceptance criteria spec §13: + +- 6 новых файлов созданы, тесты зелёные; +- lefthook job 16 red-green работает (Task 4); +- ретрофилл идемпотентен (Step 3); +- `node tools/observer-chain-map-checker.mjs` → OK; +- STATUS.md содержит строку C6. + +- [ ] **Step 6: Финальный push** + +```bash +git push origin feat/observer-chain-attribution:main +``` + +NB: gitleaks pre-push + полная регрессия по политике §15; push-паттерн `<ветка>:main` (FF). + +--- + +## Self-Review (выполнено при написании плана) + +**Spec coverage:** §4 архитектура → Tasks 1–8; §5 компоненты 1-8 → Tasks 1–8 (по одному); §6 потоки A/B/C/D → Tasks 2/4/6/5; §7 ошибки → defensive try/catch в Task 2 Step 3 (битый JSON) + Task 3 (формат .md) + Task 5 (идемпотентность); §8 тесты → Tasks 1/3/5/6/7; §10 порядок → Task 0; §13 acceptance → Task 9 Step 5. Все секции покрыты. + +**Placeholder scan:** код приведён во всех code-степах. Места «NB: имя взять из существующего теста» — намеренные адаптационные точки (фабрики транскриптов и имена функций analyzer/generateStatus зависят от финальной базы после epic-плана и не могут быть зафиксированы заранее), не placeholder-логика. + +**Type consistency:** `loadChainMap()`/`chainsFor(node, map)` — единые сигнатуры в Tasks 1/2/5/6. `parseChainsFromMd()`/`checkSync(jsonMap, mdSet)` → `{ ok, jsonOnly, mdOnly }` — единые в Tasks 3/4/7. `retrofillLine(ep, map)`/`retrofillFile(path, map, opts)` — Task 5. `chain_ref` форма (массив \| null) консистентна везде. + +**Раскрытые при детализации уточнения** (в разделе «Уточнения к spec» вверху): тесты в `tools/` не `tests/`; C6 по L-номерам не по именам узлов; врезка по grep-маркеру. diff --git a/docs/superpowers/specs/2026-05-20-observer-chain-attribution-design.md b/docs/superpowers/specs/2026-05-20-observer-chain-attribution-design.md new file mode 100644 index 00000000..7b2fc592 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-observer-chain-attribution-design.md @@ -0,0 +1,274 @@ +# 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](../../observer/notes/2026-05-20-brain-retro-v2.md) — Candidate №2 «атрибуция canonical chains L1–L13 в primary_rationale». +**Cross-refs:** [docs/routing-off-phase.md](../../routing-off-phase.md) v1.2 (L1–L13 таблица, строки 84–96); [docs/superpowers/specs/2026-05-19-observer-factor-analysis-design.md](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](../../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». Работает в трёх местах: + +1. **При записи эпизода** (Stop-хук → парсер транскрипта): после выбора `node_chosen` парсер дополнительно вычисляет `chain_ref` через чистую функцию `chainsFor(node)`. Запись в JSONL — атомарная append-line, как и сейчас. +2. **При коммите** (lefthook pre-commit): контролёр **C6** сверяет JSON-маппинг с таблицей L1–L13 в `routing-off-phase.md`. Расхождение → коммит блокируется с человеко-читаемым diff'ом. +3. **При ретро** (`/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` в памяти). 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")` → `null` +- `chainsFor("unknown-node")` → `null` +- `chainsFor("")`, `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](../../routing-off-phase.md#L84). Извлечение узлов из ячейки «Цепочка» — однократное руками при первой реализации, дальше контролёр C6 синхронизирует. + +Примерный shape (демонстрация формата; финальный JSON генерируется при реализации): + +```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](../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:** + +1. Дождаться push'а epic 20-task на `origin/main` (контролёр или владелец сообщает «epic закрыт, push сделан»). +2. `git fetch origin && git log HEAD..origin/main --oneline` — убедиться, что 20 task реально влиты. +3. Создать свежий worktree `.claude/worktrees/observer-chain-attribution/` off `origin/main` (новая ветка `feat/observer-chain-attribution`). +4. Перейти к `superpowers:writing-plans` — детальный TDD-план по компонентам §5. +5. Исполнить через `superpowers:subagent-driven-development` (Sonnet/Opus only per Pravila §15.1). +6. Финальный 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.md` v1.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_divergence` event** — заявлен в factor-analysis v1.2 §10 как phase-2 / agent-based (нужен LLM-судья «правильная ли цепочка»). Не в этом spec'е. `chain_ref` — это атрибутирование, а не суждение. +- **`triggers_matched: routing-off-phase L7` heuristic** — уже реализуется в 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 считается реализованным когда: + +1. Все 6 новых файлов из §5 созданы, тесты проходят локально и в CI. +2. Pre-commit job `chain-map-sync` врезан в `lefthook.yml`, smoke-проверка red-green работает (намеренная рассинхронизация JSON ↔ .md → коммит блокируется; правильная синхронизация → проходит). +3. `node tools/observer-retrofill-chain-ref.mjs --dry-run` показывает планируемые изменения для всех v2-эпизодов в `episodes-2026-05.jsonl`; запуск без `--dry-run` добавляет `chain_ref`; повторный запуск → 0 changes. +4. `/brain-retro` следующего spring печатает непустую секцию «Canonical chains L1–L13 hit rate» с реальными цифрами. +5. STATUS.md добавлена строка «C6 Chain map sync: ✅ — last sync OK». +6. Финальная регрессия `npm run test:tools` ≥ 331 + N (где N — число новых тестов из §8) GREEN. From 28671cb012f581c5e638efafc98db46216b213f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:47:00 +0300 Subject: [PATCH 03/11] feat(observer): chain-map JSON + chainsFor detector (L1-L13 attribution) --- tools/observer-chain-detector.mjs | 24 +++++++++++++++++ tools/observer-chain-detector.test.mjs | 32 ++++++++++++++++++++++ tools/observer-chain-map.json | 37 ++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 tools/observer-chain-detector.mjs create mode 100644 tools/observer-chain-detector.test.mjs create mode 100644 tools/observer-chain-map.json diff --git a/tools/observer-chain-detector.mjs b/tools/observer-chain-detector.mjs new file mode 100644 index 00000000..606c1de1 --- /dev/null +++ b/tools/observer-chain-detector.mjs @@ -0,0 +1,24 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_MAP_PATH = join(__dirname, 'observer-chain-map.json'); + +/** Load the node->chains map. Throws on missing/invalid JSON (caller handles). */ +export function loadChainMap(path = DEFAULT_MAP_PATH) { + const raw = JSON.parse(readFileSync(path, 'utf8')); + const map = new Map(); + for (const [node, chains] of Object.entries(raw)) { + if (node === '_note') continue; + if (Array.isArray(chains) && chains.length > 0) map.set(node, chains); + } + return map; +} + +/** node_chosen -> array of L-chains, or null if not in any chain. */ +export function chainsFor(node, map) { + if (!node || typeof node !== 'string') return null; + const chains = map.get(node); + return chains && chains.length > 0 ? chains : null; +} diff --git a/tools/observer-chain-detector.test.mjs b/tools/observer-chain-detector.test.mjs new file mode 100644 index 00000000..8ad6708f --- /dev/null +++ b/tools/observer-chain-detector.test.mjs @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; + +const map = loadChainMap(); + +describe('chainsFor', () => { + it('returns chain array for a single-chain node', () => { + expect(chainsFor('billing-audit', map)).toEqual(['L13']); + }); + + it('returns all chains for a multi-chain node', () => { + expect(chainsFor('discovery-interview', map)).toEqual(['L1', 'L2']); + }); + + it('returns null for direct', () => { + expect(chainsFor('direct', map)).toBeNull(); + }); + + it('returns null for an unknown node', () => { + expect(chainsFor('totally-unknown-xyz', map)).toBeNull(); + }); + + it('returns null for empty/null/undefined', () => { + expect(chainsFor('', map)).toBeNull(); + expect(chainsFor(null, map)).toBeNull(); + expect(chainsFor(undefined, map)).toBeNull(); + }); + + it('ignores the _note metadata key', () => { + expect(chainsFor('_note', map)).toBeNull(); + }); +}); diff --git a/tools/observer-chain-map.json b/tools/observer-chain-map.json new file mode 100644 index 00000000..32d9f873 --- /dev/null +++ b/tools/observer-chain-map.json @@ -0,0 +1,37 @@ +{ + "_note": "node_chosen -> L-цепочки. Только узлы, входящие хотя бы в одну L1-L13. Узлы вне цепочек (direct, прочее) НЕ включаются -> chainsFor вернёт null. Имена ключей = реальные значения primary_rationale.node_chosen (skill-id из skill_invoked). MCP/agent-узлы (laravel-boost, openapi-mcp-server, api-docs, sentry-mcp, redis-mcp, pest, github-mcp) в node_chosen не появляются, но включены для полноты покрытия цепочек L1-L13 (контролёр C6 требует, чтобы каждая L из routing-off-phase.md была покрыта). Синхронизируется с docs/routing-off-phase.md через tools/observer-chain-map-checker.mjs.", + "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:adr": ["L4", "L5"], + "adr-kit:judge": ["L5"], + "operations": ["L4"], + "architecture-patterns:architecture-patterns": ["L5"], + "deptrac": ["L5"], + "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:brainstorm": ["L9"], + "github-mcp": ["L9"], + "promptfoo": ["L10"], + "data-scientist": ["L10"], + "claude-api": ["L10"], + "skill-creator:skill-creator": ["L11"], + "hookify:hookify": ["L11"], + "plugin-dev:create-plugin": ["L11"], + "claude-md-management:claude-md-improver": ["L12"], + "claude-md-management:revise-claude-md": ["L12"], + "billing-audit": ["L13"], + "pest": ["L13"], + "ru-tax-accounting": ["L13"] +} From f943b229c0e0216e82ca21acbd4a41c1fb41b0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:48:21 +0300 Subject: [PATCH 04/11] feat(observer): emit chain_ref in primary_rationale --- tools/observer-transcript-parser.mjs | 9 +++++++++ tools/observer-transcript-parser.test.mjs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/tools/observer-transcript-parser.mjs b/tools/observer-transcript-parser.mjs index 748f3b03..e50113ad 100644 --- a/tools/observer-transcript-parser.mjs +++ b/tools/observer-transcript-parser.mjs @@ -16,6 +16,14 @@ */ import { detectChoiceProvenance, detectAskUserQuestionChoice } from './observer-choice-detector.mjs'; +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; + +let CHAIN_MAP = null; +try { + CHAIN_MAP = loadChainMap(); +} catch { + CHAIN_MAP = new Map(); // битый/отсутствующий JSON -> chainsFor вернёт null, observer не падает +} const SUPERPOWERS_PREFIX = 'superpowers:'; @@ -694,6 +702,7 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) { return { step: 1, node_chosen: skills.length > 0 ? skills[0] : 'direct', + chain_ref: chainsFor(skills.length > 0 ? skills[0] : 'direct', CHAIN_MAP), triggers_matched: merge(extractTriggers(turn), tag ? tag.triggers : []), candidates_considered: merge(extractCandidates(turn), tag ? tag.candidates : []), boundaries_applied: merge(extractBoundaries(turn), tag ? tag.boundaries : []), diff --git a/tools/observer-transcript-parser.test.mjs b/tools/observer-transcript-parser.test.mjs index a7df1560..09ac10e1 100644 --- a/tools/observer-transcript-parser.test.mjs +++ b/tools/observer-transcript-parser.test.mjs @@ -106,6 +106,25 @@ describe('parseTranscript', () => { expect(parseTranscript(t).primary_rationale.node_chosen).toBe('direct'); }); + it('attaches chain_ref for a node that belongs to a chain', () => { + const t = jsonl([ + userPrompt('go', '2026-05-19T10:00:00Z'), + assistantTurn( + [{ type: 'tool_use', id: 't1', name: 'Skill', input: { skill: 'billing-audit' } }], + '2026-05-19T10:01:00Z' + ), + ]); + expect(parseTranscript(t).primary_rationale.chain_ref).toEqual(['L13']); + }); + + it('sets chain_ref null for a direct episode', () => { + const t = jsonl([ + userPrompt('go', '2026-05-19T10:00:00Z'), + assistantTurn([{ type: 'tool_use', id: 't1', name: 'Read', input: {} }], '2026-05-19T10:01:00Z'), + ]); + expect(parseTranscript(t).primary_rationale.chain_ref).toBeNull(); + }); + it('hard_floor invoked when a superpowers skill is used', () => { const t = jsonl([ userPrompt('go', '2026-05-19T10:00:00Z'), From 05076c4f1df1eec7ef3372f3b2ecb6af31195ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:49:55 +0300 Subject: [PATCH 05/11] feat(observer): C6 chain-map-checker (JSON vs routing-off-phase.md sync) + L14 coverage --- tools/observer-chain-map-checker.mjs | 67 +++++++++++++++++++++++ tools/observer-chain-map-checker.test.mjs | 46 ++++++++++++++++ tools/observer-chain-map.json | 6 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tools/observer-chain-map-checker.mjs create mode 100644 tools/observer-chain-map-checker.test.mjs diff --git a/tools/observer-chain-map-checker.mjs b/tools/observer-chain-map-checker.mjs new file mode 100644 index 00000000..eb53f546 --- /dev/null +++ b/tools/observer-chain-map-checker.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Brain governance controller C6 — chain-map sync checker. + * Verifies tools/observer-chain-map.json against the L1-L13 table in + * docs/routing-off-phase.md. Sync is checked by L-number sets (both + * directions), not by node names — node_chosen values (skill-id) differ + * from the human display names in the .md table. Pure fs/regex, no LLM. + */ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const MD_PATH = join(__dirname, '..', 'docs', 'routing-off-phase.md'); +const JSON_PATH = join(__dirname, 'observer-chain-map.json'); + +/** Extract the set of L-numbers ("L1".."L13") from the routing-off-phase.md table. */ +export function parseChainsFromMd(md) { + const set = new Set(); + for (const line of md.split(/\r?\n/)) { + const m = /^\|\s*(L\d+)\s*\|/.exec(line.trim()); + if (m) set.add(m[1]); + } + return set; +} + +/** Compare JSON L-numbers against the md set, both directions. */ +export function checkSync(jsonMap, mdSet) { + const jsonSet = new Set(); + for (const [node, chains] of Object.entries(jsonMap)) { + if (node === '_note') continue; + if (Array.isArray(chains)) for (const c of chains) jsonSet.add(c); + } + const jsonOnly = [...jsonSet].filter((c) => !mdSet.has(c)); // ссылки на несуществующие L + const mdOnly = [...mdSet].filter((c) => !jsonSet.has(c)); // потерянные цепочки + return { ok: jsonOnly.length === 0 && mdOnly.length === 0, jsonOnly, mdOnly }; +} + +/** CLI entry — exit 1 on drift with a human-readable message. */ +function main() { + const md = readFileSync(MD_PATH, 'utf8'); + const jsonMap = JSON.parse(readFileSync(JSON_PATH, 'utf8')); + const mdSet = parseChainsFromMd(md); + if (mdSet.size === 0) { + console.error( + '[chain-map-checker] не нашёл ни одной L-строки в routing-off-phase.md — формат таблицы изменился?' + ); + process.exit(1); + } + const res = checkSync(jsonMap, mdSet); + if (res.ok) { + console.log(`[chain-map-checker] OK — ${mdSet.size} chains in sync`); + process.exit(0); + } + console.error('[chain-map-checker] дрейф маппинга chain-map <-> routing-off-phase.md:'); + if (res.jsonOnly.length) + console.error(` JSON ссылается на отсутствующие в .md цепочки: ${res.jsonOnly.join(', ')}`); + if (res.mdOnly.length) + console.error( + ` В .md есть цепочки без записи в JSON: ${res.mdOnly.join(', ')} — добавьте узлы в tools/observer-chain-map.json` + ); + process.exit(1); +} + +if (process.argv[1]?.endsWith('observer-chain-map-checker.mjs')) { + main(); +} diff --git a/tools/observer-chain-map-checker.test.mjs b/tools/observer-chain-map-checker.test.mjs new file mode 100644 index 00000000..da442766 --- /dev/null +++ b/tools/observer-chain-map-checker.test.mjs @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { parseChainsFromMd, checkSync } from './observer-chain-map-checker.mjs'; + +const SAMPLE_MD = [ + '| # | Цепочка | Зачем |', + '|---|---|---|', + '| L1 | `discovery-interview` (FEATURE) → `brainstorming` | text |', + '| L2 | `audit-portal` | text |', + '| L13 | `billing-audit` (#62) + `Pest` | text |', +].join('\n'); + +describe('parseChainsFromMd', () => { + it('extracts the set of L-numbers from the table', () => { + expect(parseChainsFromMd(SAMPLE_MD)).toEqual(new Set(['L1', 'L2', 'L13'])); + }); +}); + +describe('checkSync', () => { + it('passes when JSON L-numbers subset of md and md subset of json-union', () => { + const mdSet = new Set(['L1', 'L2', 'L13']); + const jsonMap = { a: ['L1'], b: ['L2'], c: ['L13'] }; + expect(checkSync(jsonMap, mdSet).ok).toBe(true); + }); + + it('fails when JSON references a chain absent from md', () => { + const mdSet = new Set(['L1', 'L2']); + const jsonMap = { a: ['L1'], b: ['L99'] }; + const res = checkSync(jsonMap, mdSet); + expect(res.ok).toBe(false); + expect(res.jsonOnly).toContain('L99'); + }); + + it('fails when md has a chain not covered by any JSON entry', () => { + const mdSet = new Set(['L1', 'L2', 'L14']); + const jsonMap = { a: ['L1'], b: ['L2'] }; + const res = checkSync(jsonMap, mdSet); + expect(res.ok).toBe(false); + expect(res.mdOnly).toContain('L14'); + }); + + it('ignores the _note metadata key in the JSON map', () => { + const mdSet = new Set(['L1']); + const jsonMap = { _note: 'meta', a: ['L1'] }; + expect(checkSync(jsonMap, mdSet).ok).toBe(true); + }); +}); diff --git a/tools/observer-chain-map.json b/tools/observer-chain-map.json index 32d9f873..e13a3acb 100644 --- a/tools/observer-chain-map.json +++ b/tools/observer-chain-map.json @@ -12,7 +12,11 @@ "adr-kit:judge": ["L5"], "operations": ["L4"], "architecture-patterns:architecture-patterns": ["L5"], - "deptrac": ["L5"], + "deptrac": ["L5", "L14"], + "rector": ["L14"], + "php-insights": ["L14"], + "larastan": ["L14"], + "laravel-backend-patterns": ["L14"], "security-review": ["L6"], "openapi-mcp-server": ["L7"], "api-docs": ["L7"], From f6ba9bc1e7b0270d32a1f2f6fd92d71b6a3040a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:50:21 +0300 Subject: [PATCH 06/11] chore(lefthook): wire C6 observer-chain-map-checker (job 16, blocking) --- lefthook.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lefthook.yml b/lefthook.yml index 0a9544f9..fff6e345 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -196,6 +196,16 @@ pre-commit: observer-coverage-checker reports a gap (coverage or registration). See docs/observer/STATUS.md C5 row for details. + # 16. observer-chain-map-checker — brain governance C6 (chain attribution). + # Сверяет tools/observer-chain-map.json с таблицей L1-L13 в + # docs/routing-off-phase.md по множествам L-номеров (обе стороны). Блокирует + # коммит при дрейфе: несуществующая L в JSON или потерянная цепочка из .md. + - name: observer-chain-map-checker + run: node tools/observer-chain-map-checker.mjs + fail_text: | + observer-chain-map-checker: дрейф chain-map <-> routing-off-phase.md. + Обновите tools/observer-chain-map.json под таблицу L1-LN. + # Post-commit: regenerate STATUS.md dashboard (informational, not gate) post-commit: parallel: false From 65c2c5e4713e707731c907da975cb0bb3d6b5020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:51:07 +0300 Subject: [PATCH 07/11] feat(observer): one-shot chain_ref retrofill script (idempotent, atomic) --- tools/observer-retrofill-chain-ref.mjs | 58 +++++++++++++++++++++ tools/observer-retrofill-chain-ref.test.mjs | 28 ++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tools/observer-retrofill-chain-ref.mjs create mode 100644 tools/observer-retrofill-chain-ref.test.mjs diff --git a/tools/observer-retrofill-chain-ref.mjs b/tools/observer-retrofill-chain-ref.mjs new file mode 100644 index 00000000..3d6df8d2 --- /dev/null +++ b/tools/observer-retrofill-chain-ref.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * One-shot retrofill: add primary_rationale.chain_ref to existing v2 episodes + * in docs/observer/episodes-*.jsonl. Idempotent (skips lines that already have + * chain_ref), atomic per file (tmp + rename). Pure fs, no LLM. + * + * Usage: node tools/observer-retrofill-chain-ref.mjs [--dry-run] + */ +import { readFileSync, writeFileSync, renameSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OBS_DIR = join(__dirname, '..', 'docs', 'observer'); + +/** Add chain_ref to a single parsed episode object (pure). Idempotent. */ +export function retrofillLine(ep, map) { + if (!ep || ep.schema_version !== 2 || !ep.primary_rationale) return ep; + if ('chain_ref' in ep.primary_rationale) return ep; // idempotent + ep.primary_rationale.chain_ref = chainsFor(ep.primary_rationale.node_chosen, map); + return ep; +} + +/** Process one JSONL file atomically (tmp + rename). Returns {changed, total}. */ +export function retrofillFile(path, map, { dryRun = false } = {}) { + const lines = readFileSync(path, 'utf8').split(/\r?\n/); + let changed = 0; + let total = 0; + const out = lines.map((line) => { + if (!line.trim()) return line; + total++; + const ep = JSON.parse(line); + const before = ep.primary_rationale && 'chain_ref' in ep.primary_rationale; + const next = retrofillLine(ep, map); + const after = next.primary_rationale && 'chain_ref' in next.primary_rationale; + if (!before && after) changed++; + return JSON.stringify(next); + }); + if (!dryRun && changed > 0) { + const tmp = `${path}.tmp`; + writeFileSync(tmp, out.join('\n'), 'utf8'); + renameSync(tmp, path); + } + return { changed, total }; +} + +function main() { + const dryRun = process.argv.includes('--dry-run'); + const map = loadChainMap(); + const files = readdirSync(OBS_DIR).filter((f) => /^episodes-\d{4}-\d{2}\.jsonl$/.test(f)); + for (const f of files) { + const { changed, total } = retrofillFile(join(OBS_DIR, f), map, { dryRun }); + console.log(`${dryRun ? '[dry-run] ' : ''}${f}: ${changed}/${total} lines get chain_ref`); + } +} + +if (process.argv[1]?.endsWith('observer-retrofill-chain-ref.mjs')) main(); diff --git a/tools/observer-retrofill-chain-ref.test.mjs b/tools/observer-retrofill-chain-ref.test.mjs new file mode 100644 index 00000000..9ba74107 --- /dev/null +++ b/tools/observer-retrofill-chain-ref.test.mjs @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { retrofillLine } from './observer-retrofill-chain-ref.mjs'; +import { loadChainMap } from './observer-chain-detector.mjs'; + +const map = loadChainMap(); + +describe('retrofillLine', () => { + it('adds chain_ref to a v2 episode with a known node', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'billing-audit' } }; + const out = retrofillLine(ep, map); + expect(out.primary_rationale.chain_ref).toEqual(['L13']); + }); + + it('sets chain_ref null for a direct v2 episode', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct' } }; + expect(retrofillLine(ep, map).primary_rationale.chain_ref).toBeNull(); + }); + + it('is idempotent — does not overwrite existing chain_ref', () => { + const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct', chain_ref: ['L1'] } }; + expect(retrofillLine(ep, map).primary_rationale.chain_ref).toEqual(['L1']); + }); + + it('skips v1 episodes (no schema_version 2)', () => { + const ep = { foo: 'bar' }; + expect(retrofillLine(ep, map)).toEqual({ foo: 'bar' }); + }); +}); From 4c9a1e9ccb89be3ea901a92c1cdf28d01b44ff72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:52:19 +0300 Subject: [PATCH 08/11] feat(brain-retro): aggregate chain_ref into factorMatrix (multi-chain axis) --- tools/brain-retro-analyzer.mjs | 12 ++++++++++++ tools/brain-retro-analyzer.test.mjs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/tools/brain-retro-analyzer.mjs b/tools/brain-retro-analyzer.mjs index 9727b35f..b4ed2008 100644 --- a/tools/brain-retro-analyzer.mjs +++ b/tools/brain-retro-analyzer.mjs @@ -177,6 +177,18 @@ export function buildFactorMatrix(episodesWithOutcome) { matrix[fname][val][outcome] = (matrix[fname][val][outcome] || 0) + 1; } } + // chain_ref is multi-value: a multi-chain episode counts once per chain; + // null/absent → key "null". Handled outside FACTOR_FNS (single-value loop). + matrix.chain_ref = {}; + for (const e of episodesWithOutcome) { + const cr = (e.primary_rationale || {}).chain_ref; + const outcome = e._inferredOutcome || 'unknown'; + const keys = Array.isArray(cr) && cr.length ? cr : ['null']; + for (const k of keys) { + matrix.chain_ref[k] = matrix.chain_ref[k] || {}; + matrix.chain_ref[k][outcome] = (matrix.chain_ref[k][outcome] || 0) + 1; + } + } return matrix; } diff --git a/tools/brain-retro-analyzer.test.mjs b/tools/brain-retro-analyzer.test.mjs index ffb59ce8..ca19b156 100644 --- a/tools/brain-retro-analyzer.test.mjs +++ b/tools/brain-retro-analyzer.test.mjs @@ -230,6 +230,23 @@ describe('buildFactorMatrix — session_segment_turn axis rename (Task 14)', () }); }); +describe('buildFactorMatrix — chain_ref axis (multi-chain)', () => { + it('counts a multi-chain episode in each chain and null for direct', () => { + const m = buildFactorMatrix([ + { _inferredOutcome: 'success', primary_rationale: { node_chosen: 'discovery-interview', chain_ref: ['L1', 'L2'] } }, + { _inferredOutcome: 'unknown', primary_rationale: { node_chosen: 'direct', chain_ref: null } }, + ]); + expect(m.chain_ref.L1).toEqual({ success: 1 }); + expect(m.chain_ref.L2).toEqual({ success: 1 }); + expect(m.chain_ref.null).toEqual({ unknown: 1 }); + }); + + it('chain_ref axis present via analyze()', () => { + const result = analyze([ep({ primary_rationale: { node_chosen: 'billing-audit', chain_ref: ['L13'], task_classification: 'other' } })]); + expect(result.factorMatrix).toHaveProperty('chain_ref'); + }); +}); + describe('inferOutcome — neutral → soft_success (Task 16)', () => { it('returns soft_success when next prompt is neutral', () => { const a = { events: [] }; From df2d0911741f89bcc69c2a319e943614efe13d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 04:54:00 +0300 Subject: [PATCH 09/11] feat(status-md): surface C6 chain-map sync row --- docs/observer/STATUS.md | 7 ++++--- tools/status-md-generator.mjs | 3 +++ tools/status-md-generator.test.mjs | 6 ++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index d9da907a..2cf07844 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-21T01:29:26.077Z +Last updated: 2026-05-21T01:53:48.034Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,11 +8,12 @@ Last updated: 2026-05-21T01:29:26.077Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ✅ | 37 episode(s) this month · Stop-hook + post-commit OK | +| C5 Observer-coverage | ⚠️ | 16 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) | +| C6 Chain map sync | ✅ | [chain-map-checker] OK — 14 chains in sync | ## Метрики (информационные, не алерты) -- Observer evidence: 37 episodes this month, 0 observer_error markers, 36 PII matches before filter +- Observer evidence: 16 episodes this month, 0 observer_error markers, 0 PII matches before filter - Legacy v1 episodes (not in factor analysis): 5 - Last /brain-retro: 2 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). diff --git a/tools/status-md-generator.mjs b/tools/status-md-generator.mjs index 86ea9bfe..b5feafed 100644 --- a/tools/status-md-generator.mjs +++ b/tools/status-md-generator.mjs @@ -10,6 +10,7 @@ function iconFor(status) { export function renderStatus(inputs) { const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo } = inputs; + const c6 = inputs.c6 || { status: 'ok', detail: '—' }; const retroLine = (lastRetroDaysAgo === null || lastRetroDaysAgo === undefined) ? 'never' : `${lastRetroDaysAgo} day(s) ago`; @@ -24,6 +25,7 @@ Last updated: ${now} | C3 Observer-of-observer | ${iconFor(c3.status)} | ${c3.detail || '—'} | | C4 Сигнальный статус | ✅ | This file (self-reference) | | C5 Observer-coverage | ${iconFor(c5.status)} | ${c5.detail || '—'} | +| C6 Chain map sync | ${iconFor(c6.status)} | ${c6.detail || '—'} | ## Метрики (информационные, не алерты) @@ -114,6 +116,7 @@ if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md- status: c5ok ? 'ok' : 'warn', detail: [cov.coverage.detail, cov.registration.detail].join(' · '), }, + c6: runControllerNode(['tools/observer-chain-map-checker.mjs']), observer: { episodeCount: countEpisodes(), observerErrors: countObserverErrors(), diff --git a/tools/status-md-generator.test.mjs b/tools/status-md-generator.test.mjs index c4c861db..daefdf5e 100644 --- a/tools/status-md-generator.test.mjs +++ b/tools/status-md-generator.test.mjs @@ -7,6 +7,7 @@ const baseInputs = (overrides = {}) => ({ c2: { status: 'ok', detail: '0 version drift' }, c3: { status: 'ok', detail: 'last read today' }, c5: { status: 'ok', detail: 'coverage OK · registration OK' }, + c6: { status: 'ok', detail: '14 chains in sync' }, observer: { episodeCount: 12, observerErrors: 0, piiMatches: 0 }, ...overrides, }); @@ -23,6 +24,11 @@ describe('renderStatus', () => { expect(md).toContain('12 episodes'); }); + it('includes a C6 chain-map row', () => { + const md = renderStatus(baseInputs()); + expect(md).toContain('| C6 Chain map sync | ✅'); + }); + it('shows a warn status for the coverage controller', () => { const md = renderStatus(baseInputs({ c5: { status: 'warn', detail: '3 commits, 0 episodes' } })); expect(md).toContain('| C5 Observer-coverage | ⚠️'); From ee5bc56f2df39b423522c21d2a1a7ba4c95893a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 06:04:23 +0300 Subject: [PATCH 10/11] docs(brain-retro): fill L1-L13+ hit rate template section --- .../brain-retro/references/aggregation-template.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.claude/skills/brain-retro/references/aggregation-template.md b/.claude/skills/brain-retro/references/aggregation-template.md index e62c38c9..dbe4c8ee 100644 --- a/.claude/skills/brain-retro/references/aggregation-template.md +++ b/.claude/skills/brain-retro/references/aggregation-template.md @@ -70,10 +70,14 @@ For each factor below, render a table: factor value × outcome counts - `observerErrorCount` from the analyzer — observer_error markers in the period. Non-zero = the observer failed silently somewhere; investigate. -## Canonical chains L1–L12 hit rate +## Canonical chains L1–L13+ hit rate (from analyzer `factorMatrix.chain_ref`) -| chain | times | notes | -|---|---|---| +| chain | times | outcome split | notes | +|---|---|---|---| + +Each node may belong to several L (a multi-chain episode is counted in each). +`null` = episodes outside any chain (`direct` + nodes not in L1–L13+) — **not a +problem** per `memory/feedback_brain_unused_tools_not_problem`. ## Improvised chains (path_type=improvised, repeated ≥2) From 54b1de78b81dc3be2a50edfb7dd184bf0de478b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 06:05:21 +0300 Subject: [PATCH 11/11] chore(observer): retrofill chain_ref on existing committed May episodes --- docs/observer/episodes-2026-05.jsonl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/observer/episodes-2026-05.jsonl b/docs/observer/episodes-2026-05.jsonl index dc80ee89..69fc3595 100644 --- a/docs/observer/episodes-2026-05.jsonl +++ b/docs/observer/episodes-2026-05.jsonl @@ -3,14 +3,14 @@ {"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T06:10:13.713Z","ended_at":"2026-05-19T06:16:11.406Z"},"path_type":"improvised","outcome":"success","primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"Write":1,"Bash":2,"Edit":3,"TodoWrite":1}},{"kind":"error","message":"tool_result reported is_error"}]} {"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T06:20:40.404Z","ended_at":"2026-05-19T06:23:08.962Z"},"path_type":"improvised","outcome":"success","primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"Bash":2,"Read":1,"Edit":2}}]} {"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T06:32:15.034Z","ended_at":"2026-05-19T06:57:02.675Z"},"path_type":"improvised","outcome":"success","primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"bugfix"},"events":[{"kind":"tool_summary","counts":{"Read":17,"ToolSearch":1,"Glob":5,"TodoWrite":4,"Grep":14,"Write":1}}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:06:30.059Z","ended_at":"2026-05-19T08:10:43.437Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":82,"parallel_session":true},"task_size":{"tool_calls":12,"files_touched":1,"files":["c:\\моя\\проекты\\портал crm\\Документация\\CLAUDE.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"bugfix"},"events":[{"kind":"tool_summary","counts":{"Edit":5,"Read":1,"Bash":4,"TodoWrite":2}},{"kind":"error","message":"tool_result reported is_error"},{"kind":"hook_fired","counts":{"PreToolUse:Read":1,"PostToolUse:Read":1,"PreToolUse:Edit":8,"PostToolUse:Edit":4,"PreToolUse:Bash":8,"PostToolUse:Bash":4,"PreToolUse:TodoWrite":2,"PostToolUse:TodoWrite":2},"errors":0},{"kind":"retry"}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:10:44.073Z","ended_at":"2026-05-19T08:13:14.644Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"subagent-driven-development"},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":83,"parallel_session":false},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature"},"events":[]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:13:37.924Z","ended_at":"2026-05-19T08:15:57.442Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"subagent-driven-development"},"environment":{"economy_level":100,"model":"claude-opus-4-7","post_compaction":true,"session_turn":84,"parallel_session":true},"task_size":{"tool_calls":6,"files_touched":2,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\reference_github.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"Bash":1,"Read":2,"Edit":3}},{"kind":"hook_fired","counts":{"PreToolUse:Bash":1,"PostToolUse:Bash":1,"PreToolUse:Read":2,"PostToolUse:Read":2,"PreToolUse:Edit":3,"PostToolUse:Edit":3},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:21:19.146Z","ended_at":"2026-05-19T08:25:57.307Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":86,"parallel_session":false},"task_size":{"tool_calls":1,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"refactor"},"events":[{"kind":"tool_summary","counts":{"AskUserQuestion":1}},{"kind":"hook_fired","counts":{"PreToolUse:AskUserQuestion":1,"PostToolUse:AskUserQuestion":1},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:25:58.145Z","ended_at":"2026-05-19T08:28:19.676Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"brainstorming"},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":87,"parallel_session":false},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature"},"events":[]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:29:06.419Z","ended_at":"2026-05-19T08:30:06.086Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":100,"model":"claude-opus-4-7","post_compaction":true,"session_turn":88,"parallel_session":false},"task_size":{"tool_calls":2,"files_touched":1,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"Bash":1,"Edit":1}},{"kind":"hook_fired","counts":{"PreToolUse:Bash":1,"PostToolUse:Bash":1,"PreToolUse:Edit":1,"PostToolUse:Edit":1},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:34:18.924Z","ended_at":"2026-05-19T08:40:38.461Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":132,"parallel_session":true},"task_size":{"tool_calls":2,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"AskUserQuestion":2}},{"kind":"hook_fired","counts":{"PreToolUse:AskUserQuestion":2,"PostToolUse:AskUserQuestion":2},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:43:39.664Z","ended_at":"2026-05-19T08:46:16.416Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"approval","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":133,"parallel_session":true},"task_size":{"tool_calls":6,"files_touched":1,"files":["c:\\моя\\проекты\\портал crm\\Документация\\docs\\superpowers\\specs\\2026-05-19-observer-factor-analysis-design.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other"},"events":[{"kind":"tool_summary","counts":{"Read":1,"Edit":4,"Grep":1}},{"kind":"hook_fired","counts":{"PreToolUse:Read":1,"PostToolUse:Read":1,"PreToolUse:Edit":8,"PostToolUse:Edit":4,"PreToolUse:Grep":1,"PostToolUse:Grep":1},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T09:21:50.135Z","ended_at":"2026-05-19T09:27:09.498Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":139,"parallel_session":true},"task_size":{"tool_calls":11,"files_touched":3,"files":["c:\\моя\\проекты\\портал crm\\Документация\\docs\\observer\\episodes-2026-05.jsonl","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\reference_github.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature"},"events":[{"kind":"tool_summary","counts":{"Bash":3,"Read":4,"Edit":4}},{"kind":"error","message":"tool_result reported is_error"},{"kind":"error","message":"tool_result reported is_error"},{"kind":"hook_fired","counts":{"PreToolUse:Bash":6,"PostToolUse:Bash":2,"PreToolUse:Read":4,"PostToolUse:Read":3,"PreToolUse:Edit":8,"PostToolUse:Edit":4},"errors":0},{"kind":"retry"},{"kind":"retry"}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T10:11:19.381Z","ended_at":"2026-05-19T10:12:06.880Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":140,"parallel_session":true},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"question"},"events":[{"kind":"hook_fired","counts":{"Stop":1},"errors":0}]} -{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T10:13:02.977Z","ended_at":"2026-05-19T10:24:02.234Z"},"path_type":"regulated","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":91,"parallel_session":true},"task_size":{"tool_calls":19,"files_touched":4,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\553717ec-bf55-43dc-8b9c-b9812711023a.jsonl","c:\\моя\\проекты\\портал crm\\Документация\\tools\\observer-transcript-parser.test.mjs","c:\\моя\\проекты\\портал crm\\Документация\\tools\\observer-transcript-parser.mjs","c:\\моя\\проекты\\портал crm\\Документация\\CLAUDE.md"]},"primary_rationale":{"step":1,"node_chosen":"superpowers:systematic-debugging","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":true,"rules":["Pravila §12"]},"task_classification":"other"},"events":[{"kind":"skill_invoked","skill":"superpowers:systematic-debugging"},{"kind":"skill_invoked","skill":"claude-md-management:claude-md-improver"},{"kind":"tool_summary","counts":{"Skill":2,"Grep":2,"Read":5,"Bash":7,"Edit":3}},{"kind":"hook_fired","counts":{"PreToolUse:Skill":2,"PostToolUse:Skill":2,"PreToolUse:Grep":2,"PostToolUse:Grep":2,"PreToolUse:Read":5,"PostToolUse:Read":5,"PreToolUse:Bash":14,"PostToolUse:Bash":7,"PreToolUse:Edit":6,"PostToolUse:Edit":3},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:06:30.059Z","ended_at":"2026-05-19T08:10:43.437Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":82,"parallel_session":true},"task_size":{"tool_calls":12,"files_touched":1,"files":["c:\\моя\\проекты\\портал crm\\Документация\\CLAUDE.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"bugfix","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"Edit":5,"Read":1,"Bash":4,"TodoWrite":2}},{"kind":"error","message":"tool_result reported is_error"},{"kind":"hook_fired","counts":{"PreToolUse:Read":1,"PostToolUse:Read":1,"PreToolUse:Edit":8,"PostToolUse:Edit":4,"PreToolUse:Bash":8,"PostToolUse:Bash":4,"PreToolUse:TodoWrite":2,"PostToolUse:TodoWrite":2},"errors":0},{"kind":"retry"}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:10:44.073Z","ended_at":"2026-05-19T08:13:14.644Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"subagent-driven-development"},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":83,"parallel_session":false},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature","chain_ref":null},"events":[]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:13:37.924Z","ended_at":"2026-05-19T08:15:57.442Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"subagent-driven-development"},"environment":{"economy_level":100,"model":"claude-opus-4-7","post_compaction":true,"session_turn":84,"parallel_session":true},"task_size":{"tool_calls":6,"files_touched":2,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\reference_github.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"Bash":1,"Read":2,"Edit":3}},{"kind":"hook_fired","counts":{"PreToolUse:Bash":1,"PostToolUse:Bash":1,"PreToolUse:Read":2,"PostToolUse:Read":2,"PreToolUse:Edit":3,"PostToolUse:Edit":3},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:21:19.146Z","ended_at":"2026-05-19T08:25:57.307Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":86,"parallel_session":false},"task_size":{"tool_calls":1,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"refactor","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"AskUserQuestion":1}},{"kind":"hook_fired","counts":{"PreToolUse:AskUserQuestion":1,"PostToolUse:AskUserQuestion":1},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:25:58.145Z","ended_at":"2026-05-19T08:28:19.676Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"user_directed_method","claude_would_have_chosen":"brainstorming"},"environment":{"economy_level":null,"model":"claude-opus-4-7","post_compaction":true,"session_turn":87,"parallel_session":false},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature","chain_ref":null},"events":[]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:29:06.419Z","ended_at":"2026-05-19T08:30:06.086Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":100,"model":"claude-opus-4-7","post_compaction":true,"session_turn":88,"parallel_session":false},"task_size":{"tool_calls":2,"files_touched":1,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"Bash":1,"Edit":1}},{"kind":"hook_fired","counts":{"PreToolUse:Bash":1,"PostToolUse:Bash":1,"PreToolUse:Edit":1,"PostToolUse:Edit":1},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:34:18.924Z","ended_at":"2026-05-19T08:40:38.461Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":132,"parallel_session":true},"task_size":{"tool_calls":2,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"AskUserQuestion":2}},{"kind":"hook_fired","counts":{"PreToolUse:AskUserQuestion":2,"PostToolUse:AskUserQuestion":2},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T08:43:39.664Z","ended_at":"2026-05-19T08:46:16.416Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"approval","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":133,"parallel_session":true},"task_size":{"tool_calls":6,"files_touched":1,"files":["c:\\моя\\проекты\\портал crm\\Документация\\docs\\superpowers\\specs\\2026-05-19-observer-factor-analysis-design.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"other","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"Read":1,"Edit":4,"Grep":1}},{"kind":"hook_fired","counts":{"PreToolUse:Read":1,"PostToolUse:Read":1,"PreToolUse:Edit":8,"PostToolUse:Edit":4,"PreToolUse:Grep":1,"PostToolUse:Grep":1},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T09:21:50.135Z","ended_at":"2026-05-19T09:27:09.498Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":139,"parallel_session":true},"task_size":{"tool_calls":11,"files_touched":3,"files":["c:\\моя\\проекты\\портал crm\\Документация\\docs\\observer\\episodes-2026-05.jsonl","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\project_brain_governance_design.md","C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\memory\\reference_github.md"]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"feature","chain_ref":null},"events":[{"kind":"tool_summary","counts":{"Bash":3,"Read":4,"Edit":4}},{"kind":"error","message":"tool_result reported is_error"},{"kind":"error","message":"tool_result reported is_error"},{"kind":"hook_fired","counts":{"PreToolUse:Bash":6,"PostToolUse:Bash":2,"PreToolUse:Read":4,"PostToolUse:Read":3,"PreToolUse:Edit":8,"PostToolUse:Edit":4},"errors":0},{"kind":"retry"},{"kind":"retry"}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T10:11:19.381Z","ended_at":"2026-05-19T10:12:06.880Z"},"path_type":"improvised","outcome":"unknown","prompt_signal":"new_task","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":140,"parallel_session":true},"task_size":{"tool_calls":0,"files_touched":0,"files":[]},"primary_rationale":{"step":1,"node_chosen":"direct","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":false,"rules":[]},"task_classification":"question","chain_ref":null},"events":[{"kind":"hook_fired","counts":{"Stop":1},"errors":0}]} +{"schema_version":2,"task_id":"553717ec-bf55-43dc-8b9c-b9812711023a","task_ref":"553717ec-bf55-43dc-8b9c-b9812711023a","timestamps":{"started_at":"2026-05-19T10:13:02.977Z","ended_at":"2026-05-19T10:24:02.234Z"},"path_type":"regulated","outcome":"unknown","prompt_signal":"neutral","decision_provenance":{"kind":"autonomous","claude_would_have_chosen":null},"environment":{"economy_level":5,"model":"claude-opus-4-7","post_compaction":true,"session_turn":91,"parallel_session":true},"task_size":{"tool_calls":19,"files_touched":4,"files":["C:\\Users\\Administrator\\.claude\\projects\\c---------------------crm-------------\\553717ec-bf55-43dc-8b9c-b9812711023a.jsonl","c:\\моя\\проекты\\портал crm\\Документация\\tools\\observer-transcript-parser.test.mjs","c:\\моя\\проекты\\портал crm\\Документация\\tools\\observer-transcript-parser.mjs","c:\\моя\\проекты\\портал crm\\Документация\\CLAUDE.md"]},"primary_rationale":{"step":1,"node_chosen":"superpowers:systematic-debugging","triggers_matched":[],"candidates_considered":[],"boundaries_applied":[],"hard_floor":{"invoked":true,"rules":["Pravila §12"]},"task_classification":"other","chain_ref":["L8"]},"events":[{"kind":"skill_invoked","skill":"superpowers:systematic-debugging"},{"kind":"skill_invoked","skill":"claude-md-management:claude-md-improver"},{"kind":"tool_summary","counts":{"Skill":2,"Grep":2,"Read":5,"Bash":7,"Edit":3}},{"kind":"hook_fired","counts":{"PreToolUse:Skill":2,"PostToolUse:Skill":2,"PreToolUse:Grep":2,"PostToolUse:Grep":2,"PreToolUse:Read":5,"PostToolUse:Read":5,"PreToolUse:Bash":14,"PostToolUse:Bash":7,"PreToolUse:Edit":6,"PostToolUse:Edit":3},"errors":0}]}