# 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 — последний.
