# Brain factor-analysis completeness — 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:** Оживить и достроить факторный анализ «мозга»: судейский исход вместо догадки, честный флаг деградации классификатора, честное извлечение дисциплины (без раздувания собственной метрики), и pivot-функция «любые срезы». **Architecture:** 4 точечных изменения в существующих чистых (pure) модулях, каждое через TDD. Новых обязательных полей схемы нет; одно производное поле `routing_signals` считается анализатором. Порядок: §3.2/§3.3 независимы; §3.1 (наполнить `outcome_reviewed`) предшествует §3.4 (движок предпочитает его). Ни одно изменение не трогает поведение блокирующих хуков (проверено в спеке §4.1). **Tech Stack:** Node ESM (`.mjs`), vitest (tools-only), чистые функции без fs/exec где возможно. **Спек:** `docs/superpowers/specs/2026-05-30-brain-factor-analysis-completeness-design.md` **Verify-формула (sentinel):** после кодовых задач — vitest tools-only GREEN per memory `feedback_vitest_sentinel_recipe.md`. --- ## Task 1: Razor-точный флаг деградации классификатора (спек §3.2) **Files:** - Modify: `tools/router-classifier.mjs` (ветка `if (!llmResult)`, строки ~653-664) - Test: `tools/router-classifier.test.mjs` - [ ] **Step 1: Написать падающий тест на parse_null → degraded:true** В `tools/router-classifier.test.mjs` (импорт `classify` уже есть в блоке строки 209): ```javascript describe('classify degraded flag (spec §3.2)', () => { const registry = { skills: [], chains: [] }; // минимальный; classifyByRegex терпит пустой it('parse_null (LLM called, garbage back) → degraded:true', async () => { let metricsSeen = false; const llmCall = async ({ onMetrics }) => { onMetrics({ latency_ms: 1234, retry_count_internal: 0 }); metricsSeen = true; return null; // распарсилось в null = мусор }; const r = await classify('почини баг в роутере', registry, { llmCall }); expect(metricsSeen).toBe(true); expect(r.degraded).toBe(true); expect(r.llm_error_type).toBe('parse_null'); }); it('no_key (LLM never called) → degraded:false', async () => { const llmCall = async () => null; // onMetrics НЕ вызывается → metrics остаётся null const r = await classify('почини баг в роутере', registry, { llmCall }); expect(r.degraded).toBe(false); expect(r.llm_error_type).toBe('no_key'); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/router-classifier.test.mjs -t "degraded flag"` Expected: FAIL — `r.degraded` is `undefined` (поле не выставляется в ветке `!llmResult`), оба assert на degraded падают. - [ ] **Step 3: Минимальная правка** В `tools/router-classifier.mjs`, ветка `if (!llmResult)` — добавить строку `degraded`: ```javascript if (!llmResult) { // Layer 3 — regex fallback on no key (metrics null) / unparseable response. // parse_null (metrics set ⇒ LLM was called, returned garbage) = real silent // degradation. no_key (metrics null ⇒ LLM never called) = штатный regex, NOT degraded. const r = classifyByRegex(prompt, registry); return { ...r, llm_error_type: metrics ? 'parse_null' : 'no_key', latency_ms: metrics?.latency_ms ?? null, retry_count_internal: metrics?.retry_count_internal ?? null, degraded: metrics ? true : false, }; } ``` - [ ] **Step 4: Запустить — убедиться, что проходит** Run: `npx vitest run tools/router-classifier.test.mjs -t "degraded flag"` Expected: PASS (2 теста). - [ ] **Step 5: Прогнать весь файл (нет регрессии)** Run: `npx vitest run tools/router-classifier.test.mjs` Expected: PASS все (новые 2 + существующие). - [ ] **Step 6: Commit** ```bash git add tools/router-classifier.mjs tools/router-classifier.test.mjs git commit -m "fix(router): degraded:true on silent LLM->regex fallback (parse_null), false on no_key" ``` --- ## Task 2: `resolvedOutcome` хелпер + предпочтение судейского вердикта (спек §3.4 баг B) **Files:** - Modify: `tools/brain-retro-analyzer.mjs` (рядом с `buildFactorMatrix`, строки ~383-408) - Test: `tools/brain-retro-analyzer.test.mjs` - [ ] **Step 1: Написать падающий тест на resolvedOutcome + factorMatrix** В `tools/brain-retro-analyzer.test.mjs` (там уже импортируется `buildFactorMatrix`; добавить `resolvedOutcome` в импорт из `./brain-retro-analyzer.mjs`): ```javascript describe('resolvedOutcome (spec §3.4 bug B)', () => { it('prefers outcome_reviewed over _inferredOutcome', () => { expect(resolvedOutcome({ outcome_reviewed: 'rework', _inferredOutcome: 'success' })).toBe('rework'); }); it('falls back to _inferredOutcome when no reviewed verdict', () => { expect(resolvedOutcome({ _inferredOutcome: 'success' })).toBe('success'); }); it('defaults to unknown when neither present', () => { expect(resolvedOutcome({})).toBe('unknown'); }); it('buildFactorMatrix buckets by reviewed verdict, not heuristic', () => { const eps = [ { schema_version: 4, outcome_reviewed: 'rework', _inferredOutcome: 'success', primary_rationale: { node_chosen: 'direct' } }, ]; const m = buildFactorMatrix(eps); expect(m.node_chosen.direct.rework).toBe(1); expect(m.node_chosen.direct.success).toBeUndefined(); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs -t "resolvedOutcome"` Expected: FAIL — `resolvedOutcome is not a function` (ещё не экспортирована); factorMatrix-тест падает, т.к. сейчас матрица читает `_inferredOutcome` → попадёт в `success`. - [ ] **Step 3: Добавить хелпер и применить в buildFactorMatrix** В `tools/brain-retro-analyzer.mjs` перед `buildFactorMatrix` добавить: ```javascript /** * Resolve an episode's outcome for the factor engine: prefer the reviewer's * judged verdict (outcome_reviewed), fall back to the tone-based heuristic * (_inferredOutcome), then 'unknown'. Spec §3.4 bug B. */ export function resolvedOutcome(e) { return (e && e.outcome_reviewed) || (e && e._inferredOutcome) || 'unknown'; } ``` В `buildFactorMatrix` заменить чтение исхода: ```javascript const val = fn(e); const outcome = resolvedOutcome(e); ``` И в блоке `matrix.chain_ref` (строка ~400) заменить: ```javascript const outcome = resolvedOutcome(e); ``` - [ ] **Step 4: Запустить — убедиться, что проходит** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs -t "resolvedOutcome"` Expected: PASS (4 теста). - [ ] **Step 5: Прогнать весь файл (нет регрессии существующих matrix-тестов)** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs` Expected: PASS все. (Существующие тесты используют `_inferredOutcome` без `outcome_reviewed` → resolvedOutcome падает на эвристику, поведение прежнее.) - [ ] **Step 6: Commit** ```bash git add tools/brain-retro-analyzer.mjs tools/brain-retro-analyzer.test.mjs git commit -m "feat(brain-retro): resolvedOutcome — factor matrix prefers judged verdict over heuristic" ``` --- ## Task 3: `crossTab` — pivot «любые срезы» (спек §3.4) **Files:** - Modify: `tools/brain-retro-analyzer.mjs` (после `buildFactorMatrix`) - Test: `tools/brain-retro-analyzer.test.mjs` - [ ] **Step 1: Написать падающий тест на crossTab** Добавить `crossTab` в импорт из `./brain-retro-analyzer.mjs`. Тест: ```javascript describe('crossTab (spec §3.4)', () => { const eps = [ { schema_version: 4, outcome_reviewed: 'success', environment: { economy_level: 0 }, primary_rationale: { node_chosen: 'direct' } }, { schema_version: 4, outcome_reviewed: 'rework', environment: { economy_level: 0 }, primary_rationale: { node_chosen: 'direct' } }, { schema_version: 4, outcome_reviewed: 'success', environment: { economy_level: 100 }, primary_rationale: { node_chosen: 'superpowers:brainstorming' } }, ]; it('cross-tabs two axes into outcome distributions', () => { const t = crossTab(eps, 'economy_level', 'node_chosen'); expect(t['0|direct']).toEqual({ success: 1, rework: 1 }); expect(t['100|superpowers:brainstorming']).toEqual({ success: 1 }); }); it('unknown axis name → empty object, no throw', () => { expect(crossTab(eps, 'no_such_axis', 'node_chosen')).toEqual({}); }); it('optional third axis produces triple key', () => { const t = crossTab(eps, 'economy_level', 'node_chosen', 'node_chosen'); expect(t['0|direct|direct']).toEqual({ success: 1, rework: 1 }); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs -t "crossTab"` Expected: FAIL — `crossTab is not a function`. - [ ] **Step 3: Реализовать crossTab** В `tools/brain-retro-analyzer.mjs` после `buildFactorMatrix` добавить: ```javascript /** * Pivot any 2 (or 3) factor axes against the resolved outcome (spec §3.4). * Reuses FACTOR_FNS. Returns { "valA|valB[|valC]": { outcome: count, ... } }. * Unknown axis name → {} (no throw). Outcome via resolvedOutcome (judged>heuristic). */ export function crossTab(episodes, axisA, axisB, axisC = null) { const fnA = FACTOR_FNS[axisA]; const fnB = FACTOR_FNS[axisB]; const fnC = axisC ? FACTOR_FNS[axisC] : null; if (!fnA || !fnB || (axisC && !fnC)) return {}; const out = {}; for (const e of episodes || []) { const parts = [fnA(e), fnB(e)]; if (fnC) parts.push(fnC(e)); const key = parts.join('|'); const outcome = resolvedOutcome(e); out[key] = out[key] || {}; out[key][outcome] = (out[key][outcome] || 0) + 1; } return out; } ``` - [ ] **Step 4: Запустить — убедиться, что проходит** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs -t "crossTab"` Expected: PASS (3 теста). - [ ] **Step 5: Прогнать весь файл** Run: `npx vitest run tools/brain-retro-analyzer.test.mjs` Expected: PASS все. - [ ] **Step 6: Commit** ```bash git add tools/brain-retro-analyzer.mjs tools/brain-retro-analyzer.test.mjs git commit -m "feat(brain-retro): crossTab — pivot any 2-3 factor axes against outcome" ``` --- ## Task 4: Честное извлечение дисциплины — Слой A (спек §3.3) **Files:** - Modify: `tools/observer-transcript-parser.mjs` (`extractTriggers` строки ~536-547, `extractBoundaries` ~583-594, `primary_rationale` блок ~928-953) - Test: `tools/observer-transcript-parser.test.mjs` Цель: (1) расширить `triggers_matched`/`boundaries_applied` по ДОСЛОВНЫМ цитатам (имена узлов, hard-floor/hard-rule) — это всё ещё честный текст ответа; (2) добавить ОТДЕЛЬНОЕ производное поле `primary_rationale.routing_signals` из объективных данных (`chain_ref`/`recommended_node`), НЕ вливая его в triggers. - [ ] **Step 1: Написать падающий тест на расширенные триггеры + routing_signals** В `tools/observer-transcript-parser.test.mjs` (импорт `parseTranscript`, `extractTriggers` уже могут быть; добавить при необходимости): ```javascript describe('discipline extraction Layer A (spec §3.3)', () => { it('extractTriggers catches node names and hard-floor verbatim', () => { const turn = [{ message: { role: 'assistant', content: [ { type: 'text', text: 'Применяю superpowers:brainstorming и узел #19, это hard-floor.' }, ] } }]; const t = extractTriggers(turn); expect(t).toContain('superpowers:brainstorming'); expect(t).toContain('#19'); expect(t.some((x) => /hard-floor/i.test(x))).toBe(true); }); it('routing_signals separate from triggers_matched (objective data, not prose)', () => { // transcript with a Skill invocation that maps to a chain → chain_ref non-empty const transcript = JSON.stringify({ sessionId: 's1', timestamp: '2026-05-30T10:00:00Z', message: { role: 'user', content: 'сделай фичу X с тремя шагами' }, }) + '\n' + JSON.stringify({ sessionId: 's1', timestamp: '2026-05-30T10:01:00Z', message: { role: 'assistant', content: [ { type: 'tool_use', name: 'Skill', input: { skill: 'superpowers:writing-plans' } }, ] }, }); const ep = parseTranscript(transcript, 's1'); expect(Array.isArray(ep.primary_rationale.routing_signals)).toBe(true); // triggers_matched stays prose-only — no chain_ref leakage into it expect(ep.primary_rationale.triggers_matched.every((t) => typeof t === 'string')).toBe(true); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/observer-transcript-parser.test.mjs -t "discipline extraction Layer A"` Expected: FAIL — узлы/hard-floor не ловятся текущими `TRIGGER_PATTERNS`; `routing_signals` отсутствует в `primary_rationale`. - [ ] **Step 3: Расширить паттерны и добавить routing_signals** В `tools/observer-transcript-parser.mjs`: (a) В `TRIGGER_PATTERNS` (строка ~526) добавить паттерны для имён узлов и hard-floor: ```javascript const TRIGGER_PATTERNS = [ /\bPravila\s+§\d+(?:\.\d+)?/g, /\bADR-\d+/g, /\bPSR_v1\s+R\d+(?:\.\d+)?/g, /\brouting-off-phase\s+L\d+/g, /\bL\d+\s+chain/g, /\bhard-(?:floor|rule)\b/gi, /\b[a-z][a-z0-9-]*:[a-z][a-z0-9-]*\b/g, // namespaced skill e.g. superpowers:brainstorming /(?:^|\s)#\d{1,3}\b/g, // tooling node id e.g. #19 ]; ``` NB: после `text.match` для `#NN` остаётся ведущий пробел — нормализовать в `extractTriggers` через `.trim()` при `out.add`: ```javascript if (matches) for (const m of matches) { const cleaned = m.trim(); const norm = /^L\d+\s+chain$/.test(cleaned) ? `routing-off-phase ${cleaned.split(/\s+/)[0]}` : cleaned; out.add(norm); } ``` (b) В блоке `primary_rationale` (строка ~937, объект `return {`) добавить отдельное поле ПОСЛЕ `boundaries_applied`: ```javascript boundaries_applied: merge(extractBoundaries(turn), tag ? tag.boundaries : []), routing_signals: (() => { const sig = []; const cr = chainsFor(skills.length > 0 ? skills[0] : 'direct', CHAIN_MAP); if (Array.isArray(cr) && cr.length) sig.push('chain_ref'); if (routerFields.recommended_node) sig.push('recommended_node'); return sig; })(), ``` - [ ] **Step 4: Запустить — убедиться, что проходит** Run: `npx vitest run tools/observer-transcript-parser.test.mjs -t "discipline extraction Layer A"` Expected: PASS (2 теста). - [ ] **Step 5: Прогнать весь файл (нет регрессии парсера)** Run: `npx vitest run tools/observer-transcript-parser.test.mjs` Expected: PASS все. Если существующий тест проверял точный набор triggers — обновить ожидание (новые паттерны могут добавить узлы); это ожидаемо, не регресс. - [ ] **Step 6: Commit** ```bash git add tools/observer-transcript-parser.mjs tools/observer-transcript-parser.test.mjs git commit -m "feat(observer): Layer A discipline extraction — node/hard-floor triggers + separate routing_signals" ``` --- ## Task 5: Честный watchdog — Слой B (спек §3.3) **Files:** - Modify: `tools/discipline-metrics.mjs` (`routerStepReached` строки ~81-91) - Modify: `tools/status-md-generator.mjs` (формулировка suspicious, строка ~332-334) - Test: `tools/discipline-metrics.test.mjs` Цель: НЕ ослаблять порог. `suspicious` остаётся при том же условии, но смысл честный: «низкая дисциплина ИЛИ нет объективных сигналов», а не «баг парсера». Учесть `routing_signals` — если они есть, эпизод не считается «застрявшим вслепую». - [ ] **Step 1: Написать падающий тест** В `tools/discipline-metrics.test.mjs`: ```javascript describe('routerStepReached honest watchdog (spec §3.3 Layer B)', () => { const e = (pr) => ({ schema_version: 4, primary_rationale: pr }); it('suspicious when >90% step=1 AND no routing_signals', () => { const eps = Array.from({ length: 10 }, () => e({ task_classification: 'other', triggers_matched: [], node_chosen: 'direct', chain_ref: null, routing_signals: [] })); const r = routerStepReached(eps); expect(r.suspicious).toBe(true); }); it('NOT suspicious when routing_signals present despite empty triggers', () => { const eps = Array.from({ length: 10 }, () => e({ task_classification: 'other', triggers_matched: [], node_chosen: 'direct', chain_ref: null, routing_signals: ['recommended_node'] })); const r = routerStepReached(eps); expect(r.suspicious).toBe(false); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/discipline-metrics.test.mjs -t "honest watchdog"` Expected: FAIL — второй тест падает: текущий `routerStepReached` не смотрит `routing_signals`, поднимает suspicious в обоих случаях. - [ ] **Step 3: Учесть routing_signals в suspicious** В `tools/discipline-metrics.mjs`, `routerStepReached`: ```javascript export function routerStepReached(episodes) { const distribution = {}; let total = 0; let withRoutingSignals = 0; for (const e of valid(episodes)) { const key = String(deriveRouterStep(e.primary_rationale)); distribution[key] = (distribution[key] || 0) + 1; total += 1; const sig = (e.primary_rationale || {}).routing_signals; if (Array.isArray(sig) && sig.length > 0) withRoutingSignals += 1; } const stuckAt1 = (distribution['1'] || 0) / Math.max(total, 1); const signalRate = withRoutingSignals / Math.max(total, 1); // Honest watchdog (spec §3.3 Layer B): low textual discipline AND no objective // routing signals. Objective signals present → not "stuck blind" → not suspicious. const suspicious = total >= 5 && stuckAt1 > 0.9 && signalRate < 0.1; return { distribution, total, suspicious, signalRate }; } ``` - [ ] **Step 4: Обновить формулировку в STATUS** В `tools/status-md-generator.mjs` строка ~332, заменить текст: ```javascript const suspicious = discipline.routerStep?.suspicious ? ' ⚠️ low routing discipline — >90% step=1 И нет объективных сигналов роутинга (chain_ref/recommended_node)' : ''; ``` - [ ] **Step 5: Запустить тесты** Run: `npx vitest run tools/discipline-metrics.test.mjs -t "honest watchdog"` Expected: PASS (2 теста). - [ ] **Step 6: Прогнать оба файла (нет регрессии)** Run: `npx vitest run tools/discipline-metrics.test.mjs tools/status-md-generator.test.mjs` Expected: PASS все. Если существующий status-md тест ассертил старый текст suspicious — обновить на новую формулировку. - [ ] **Step 7: Commit** ```bash git add tools/discipline-metrics.mjs tools/status-md-generator.mjs tools/discipline-metrics.test.mjs git commit -m "feat(brain): honest watchdog — suspicious only when no objective routing signals; truthful STATUS wording" ``` --- ## Task 6: `--meaningful-only` фильтр в batch-reviewer (спек §3.1) **Files:** - Modify: `tools/brain-retro-batch-reviewer.mjs` (фильтр targets, строки ~33-44; argv ~21) - Test: `tools/brain-retro-batch-reviewer.test.mjs` (создать, если отсутствует — проверить Glob) NB: основной модуль — CLI-скрипт с top-level await. Чтобы юнит-тестировать фильтр, вынести его в чистую экспортируемую функцию. - [ ] **Step 1: Написать падающий тест на isMeaningfulEpisode** Создать/дополнить `tools/brain-retro-batch-reviewer.test.mjs`: ```javascript import { describe, it, expect } from 'vitest'; import { isMeaningfulEpisode } from './brain-retro-batch-reviewer.mjs'; describe('isMeaningfulEpisode (spec §3.1)', () => { it('node_chosen != direct → meaningful', () => { expect(isMeaningfulEpisode({ primary_rationale: { node_chosen: 'superpowers:brainstorming' }, task_size: { tool_calls: 2 } })).toBe(true); }); it('direct + large task (tool_calls>=20) → meaningful', () => { expect(isMeaningfulEpisode({ primary_rationale: { node_chosen: 'direct' }, task_size: { tool_calls: 25 } })).toBe(true); }); it('direct + small task → NOT meaningful', () => { expect(isMeaningfulEpisode({ primary_rationale: { node_chosen: 'direct' }, task_size: { tool_calls: 3 } })).toBe(false); }); it('missing fields → NOT meaningful (safe default)', () => { expect(isMeaningfulEpisode({})).toBe(false); }); }); ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `npx vitest run tools/brain-retro-batch-reviewer.test.mjs` Expected: FAIL — `isMeaningfulEpisode is not a function` (не экспортирована). - [ ] **Step 3: Вынести и экспортировать чистую функцию + применить флаг** В `tools/brain-retro-batch-reviewer.mjs` ДО CLI-блока (перед чтением argv) добавить: ```javascript /** * Meaningful = a real routing decision worth reviewing (spec §3.1): * node_chosen != 'direct' (skill/node was used) OR a large task (tool_calls >= 20). * Pure — chatter (direct + small) is skipped to save reviewer cost. */ export function isMeaningfulEpisode(ep) { const node = ep?.primary_rationale?.node_chosen; if (node && node !== 'direct') return true; const calls = Number(ep?.task_size?.tool_calls) || 0; return calls >= 20; } ``` В argv-парсинге добавить флаг (после `concStr`): ```javascript const meaningfulOnly = process.argv.includes('--meaningful-only'); ``` В цикле сбора `targets` добавить условие ПОСЛЕ `if (ep.outcome_reviewed) continue;`: ```javascript if (meaningfulOnly && !isMeaningfulEpisode(ep)) continue; ``` (флаг `--meaningful-only` не должен попасть в позиционные argv: он уже отфильтруется `parseInt` для limit/conc, т.к. идёт как именованный; но для чистоты — оставить позиционные `[limitStr, concStr]` как есть, они читаются по индексу 4/5, а `--meaningful-only` встанет индексом 6+, не мешает). - [ ] **Step 4: Запустить — убедиться, что проходит** Run: `npx vitest run tools/brain-retro-batch-reviewer.test.mjs` Expected: PASS (4 теста). - [ ] **Step 5: Commit** ```bash git add tools/brain-retro-batch-reviewer.mjs tools/brain-retro-batch-reviewer.test.mjs git commit -m "feat(brain-retro): --meaningful-only filter for batch reviewer (node!=direct OR large task)" ``` --- ## Task 7: Прогон reviewer по содержательным эпизодам мая (спек §3.1, наполнение данных) **Files:** - Read/Modify (data): `docs/observer/episodes-2026-05.jsonl` (in-place построчно — пишет сам reviewer) NB: требует `ROUTER_LLM_KEY` в окружении. Это НЕ кодовая задача — прогон утилиты для наполнения данных. Должен идти ПОСЛЕ Task 6 (флаг) и предшествовать пользе от Task 2/3 (движок предпочтёт `outcome_reviewed`). - [ ] **Step 1: Сухой подсчёт — сколько содержательных непроверенных** Run: `npx vitest run` НЕ нужен. Подсчёт — через уже существующий dry-вывод reviewer'а (он печатает `total in period unreviewed` в stderr). Запуск с лимитом 0 для оценки: Run: `node tools/brain-retro-batch-reviewer.mjs docs/observer/episodes-2026-05.jsonl 2026-05-01T00:00:00Z 0 5 --meaningful-only` Expected: stderr `[batch-reviewer] total in period unreviewed: N, processing first 0 ...` — N даёт ориентир (ожидаемо ~60-90). - [ ] **Step 2: Подтвердить у заказчика бюджет прогона** Сообщить N и оценку стоимости (N × ~$0.05-0.07 на Opus, или дешевле если `REVIEWER_MODEL`=Sonnet). Дождаться «да». - [ ] **Step 3: Прогон** Run: `node tools/brain-retro-batch-reviewer.mjs docs/observer/episodes-2026-05.jsonl 2026-05-01T00:00:00Z 200 5 --meaningful-only` Expected: stderr прогресс-строки; финал `done: M reviewed, K errors`. Файл перезаписан построчно (только отревьюенные строки изменены). - [ ] **Step 4: Verify наполнения** Через Grep: `"outcome_reviewed":"` в `docs/observer/episodes-2026-05.jsonl` — count вырос с 0 до ~M. И `"review":{` — count ~M (был 0). - [ ] **Step 5: Commit данных** ```bash git add docs/observer/episodes-2026-05.jsonl git commit -m "data(observer): backfill reviewer verdicts on meaningful May episodes" ``` --- ## Task 8: Регрессия + перегенерация STATUS + нормативка **Files:** - Regenerate: `docs/observer/STATUS.md` (через генератор) - Modify: `CLAUDE.md` (§6 +абзац, §9 +entry — через `/claude-md-management:revise-claude-md`) - [ ] **Step 1: Полная регрессия tools-only** Run: `npx vitest run tools/` (исключая широкий прогон per sentinel-формулы; см. memory `feedback_vitest_sentinel_recipe.md`) Expected: GREEN, прежнее число тестов + новые (~15: 2+4+3+2+2+4 минус пересечения). - [ ] **Step 2: Перегенерировать STATUS.md и глазами проверить ожившие блоки** Run: `node tools/status-md-generator.mjs` Expected: `[status-md-generator] OK`. В `docs/observer/STATUS.md`: блок «Reviewer findings» больше не «нет проверенных», «Метрики дисциплины» с новым столбцом/честной формулировкой suspicious. - [ ] **Step 3: Commit STATUS** ```bash git add docs/observer/STATUS.md git commit -m "chore(observer): regenerate STATUS with revived reviewer + honest discipline blocks" ``` - [ ] **Step 4: Нормативка CLAUDE.md** Через `/claude-md-management:revise-claude-md`: §6 +абзац (что сделано), §9 +entry. §0 cross-refs НЕ менять (infrastructure layer, не tooling-канон/ADR/off-phase — см. спек §6). - [ ] **Step 5: Финальный verify + push** Прогнать sentinel, затем push (docs+code) — через AskUser git-approval per gate. --- ## Self-Review (выполнено при написании плана) **1. Покрытие спека:** §3.1→Task 6+7; §3.2→Task 1; §3.3 Слой A→Task 4, Слой B→Task 5; §3.4 crossTab→Task 3, resolvedOutcome(баг B)→Task 2; §4.1/§4.2 (соседи/конфликт интересов)→учтены в дизайне Task 4/5 (routing_signals отдельно, watchdog не ослаблен). §2 (YAGNI — #4/эмбеддинги/тег) — намеренно нет задач. ✅ Все требования покрыты. **2. Placeholder scan:** нет TBD/«handle edge cases» — весь код в шагах конкретный. ✅ **3. Type consistency:** `resolvedOutcome` определён в Task 2, используется в Task 3 (`crossTab`) — сигнатура совпадает. `isMeaningfulEpisode` — Task 6, используется в Task 7 (CLI). `routing_signals` — пишется Task 4 (parser), читается Task 5 (`routerStepReached`). ✅ **Порядок исполнения:** Task 1 / 2 / 3 / 4 / 5 / 6 независимы по коду (можно параллельно), НО Task 7 (прогон данных) требует Task 6, и польза Task 2/3 проявляется только после Task 7. Task 8 — последний.