Files
brain/docs/superpowers/plans/2026-06-15-task56-statedir-classifiercontext-plan-v2.md
T

12 KiB
Raw Blame History

Фаза 1 config-seam — state_dir резолвер + classifier_context параметр — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной — субагенты запрещены, VA-4). Steps — checkbox (- [ ]).

Goal: Добавить чистый fail-safe резолвер resolveStateDir в brain-config и параметр classifierContext в два prompt-builder'а (router-classifier, brain-retro-opus-reviewer), не меняя поведение claude-brain (backward-compat: дефолт = текущая строка/каталог).

Architecture: Чистые pure-seam'ы — новые параметры со значением по умолчанию / новая чистая функция. Ни один файл не gated (не discipline-source / не normative). Подключение в main() — отдельная задача (wiring).

Tech Stack: Node.js ESM (tools/), vitest (vitest.config.tools.mjs).

Спек: docs/superpowers/specs/2026-06-15-task56-statedir-classifiercontext-spec-v2.md (§D1 контракт, §D2 fail-safe state_dir, §D3 крайние случаи + критерий).

Цель

Закрыть два чистых config-seam ключа Фазы 1 (state_dir резолвер + classifier_context) на уровне pure-функций, backward-compat (дефолт = текущее значение). Wiring и project_url_whitelist — отдельно.

Переговоры

Позиция контроллера по типовым замечаниям ревью к правке существующих файлов:

  1. Edit, не Write-overwrite. Все три файла прочитаны в этой сессии; old_string каждого Edit — байт-точная подстрока текущего состояния. Правки аддитивны (новый параметр со значением по умолчанию / новый экспорт) — существующие сигнатуры/вызовы с одним аргументом не ломаются.
  2. Бэкап — git. Файлы под версионным контролем; откат — git restore / git show HEAD:<путь>.
  3. Backward-compat доказывается тестами. Каждая правка: RED→GREEN + кейс «дефолт = текущая строка байт-в-байт» + полный регресс. Авторитетный полный свод — в терминале владельца.
  4. fail-safe направление. resolveStateDir при пустом/невалидном входе возвращает безопасный дефолт + warnedFallback: true (не тихий no-op) — §5.1.
["test-driven-development"]
[
  {"op":"Edit","object":"tools/brain-config.test.mjs","ref":"D1"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/brain-config.mjs","ref":"D1"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/router-classifier.test.mjs","ref":"D2"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/router-classifier.mjs","ref":"D2"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/brain-retro-opus-reviewer.test.mjs","ref":"D3"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/brain-retro-opus-reviewer.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/brain-retro-opus-reviewer.mjs","ref":"D3"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
  {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"}
]
[
  {"id":"D1","kind":"EXTRACTED","ref":"tools/brain-config.mjs","anchor":"const DEFAULTS = Object.freeze({"},
  {"id":"D2","kind":"EXTRACTED","ref":"tools/router-classifier.mjs","anchor":"export function buildClassifierPromptStructured"},
  {"id":"D3","kind":"EXTRACTED","ref":"tools/brain-retro-opus-reviewer.mjs","anchor":"export function buildReviewPromptStructured"}
]

Task A: resolveStateDir в brain-config (fail-safe §5.1)

Files: Modify tools/brain-config.mjs, tools/brain-config.test.mjs

  • Step 1 (Edit test, RED): в tools/brain-config.test.mjs добавить в конец (отдельный import + describe):
import { resolveStateDir } from './brain-config.mjs';

describe('resolveStateDir fail-safe (§D2)', () => {
  it('непустая строка → как есть, без fallback', () => {
    expect(resolveStateDir('docs/observer')).toEqual({ stateDir: 'docs/observer', warnedFallback: false });
  });
  it('пусто / пробелы → безопасный дефолт + warnedFallback', () => {
    expect(resolveStateDir('')).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true });
    expect(resolveStateDir('   ')).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true });
  });
  it('не-строка → безопасный дефолт + warnedFallback (не падает)', () => {
    expect(resolveStateDir(null)).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true });
    expect(resolveStateDir(undefined)).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true });
  });
});
  • Step 2 (Bash, verify FAIL): npx vitest run --config vitest.config.tools.mjs Expected: новый тест FAIL (resolveStateDir is not a function). Авторитетно — терминал владельца.

  • Step 3 (Edit impl): в tools/brain-config.mjs добавить функцию в конец (после loadConfig; old_string = последние строки loadConfigreturn resolveConfig(parseBrainConfig(md));\n}):

  return resolveConfig(parseBrainConfig(md));
}

/** fail-safe резолвер state_dir (§5.1): непустая строка → как есть; иначе → безопасный дефолт
 *  .claude/brain-state + warnedFallback (НЕ тихий no-op — wiring издаёт warn и пишет в fallback). */
export function resolveStateDir(value) {
  const v = typeof value === 'string' ? value.trim() : '';
  if (v.length > 0) return { stateDir: v, warnedFallback: false };
  return { stateDir: '.claude/brain-state', warnedFallback: true };
}
  • Step 4 (Bash, verify PASS): npx vitest run --config vitest.config.tools.mjs Expected: PASS (новые + существующие).

Task B: classifierContext в router-classifier

Files: Modify tools/router-classifier.mjs, tools/router-classifier.test.mjs

  • Step 5 (Edit test, RED): в tools/router-classifier.test.mjs добавить describe (импорт buildClassifierPromptStructured уже есть в файле — иначе добавить import { buildClassifierPromptStructured } from './router-classifier.mjs'; рядом):
describe('buildClassifierPromptStructured classifierContext (config-seam §D1)', () => {
  const reg = { nodes: [], chains: {} };
  it('дефолт → текущая строка «Лидерра»', () => {
    expect(buildClassifierPromptStructured('p', reg).system).toContain('«Лидерра»');
  });
  it('classifierContext инъектируется', () => {
    expect(buildClassifierPromptStructured('p', reg, { classifierContext: 'ТестПроект XYZ' }).system)
      .toContain('ТестПроект XYZ');
  });
});
  • Step 6 (Bash, verify FAIL): npx vitest run --config vitest.config.tools.mjs Expected: FAIL (инъекция-кейс — параметр игнорируется).

  • Step 7 (Edit impl): в tools/router-classifier.mjs заменить начало buildClassifierPromptStructured (old_string = строки от сигнатуры до строки const system = Ты классификатор задач для CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3).`):

export function buildClassifierPromptStructured(userPrompt, registry, { enrichment = true, classifierContext = 'CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)' } = {}) {
  const pamyatka = enrichment ? `\n\n${PAMYATKA}\n` : '\n';
  const nodesBlock = buildNodesBlock(registry);
  const chainsBlock = buildChainsBlock(registry);

  const system = `Ты классификатор задач для ${classifierContext}.
  • Step 8 (Bash, verify PASS): npx vitest run --config vitest.config.tools.mjs Expected: PASS (дефолт = байт-в-байт текущая строка; buildClassifierPrompt вызывает без classifierContext → дефолт).

Task C: classifierContext в brain-retro-opus-reviewer

Files: Modify tools/brain-retro-opus-reviewer.mjs, tools/brain-retro-opus-reviewer.test.mjs

  • Step 9 (Edit test, RED): в tools/brain-retro-opus-reviewer.test.mjs добавить describe (импорт buildReviewPromptStructured уже есть — иначе добавить):
describe('buildReviewPromptStructured classifierContext (config-seam §D1)', () => {
  it('дефолт → Лидерра', () => {
    expect(buildReviewPromptStructured({ schema_version: 4 }).system).toContain('Лидерра');
  });
  it('classifierContext инъектируется', () => {
    expect(buildReviewPromptStructured({ schema_version: 4 }, { classifierContext: 'ProjZ' }).system)
      .toContain('ProjZ');
  });
});
  • Step 10 (Bash, verify FAIL): npx vitest run --config vitest.config.tools.mjs Expected: FAIL (инъекция-кейс).

  • Step 11 (Edit impl — сигнатура): в tools/brain-retro-opus-reviewer.mjs заменить строку сигнатуры (old_string = export function buildReviewPromptStructured(episode) {):

export function buildReviewPromptStructured(episode, { classifierContext = 'Лидерра' } = {}) {
  • Step 12 (Edit impl — строка system): заменить захардкоженную строку (old_string = 'You are the independent reviewer of routing decisions for the Лидерра brain-governance experiment.',):
    `You are the independent reviewer of routing decisions for the ${classifierContext} brain-governance experiment.`,
  • Step 13 (Bash, verify PASS): npx vitest run --config vitest.config.tools.mjs Expected: PASS (дефолт Лидерра = байт-в-байт).

Task D: финальный регресс

  • Step 14 (Bash, полный регресс): npx vitest run --config vitest.config.tools.mjs Expected: PASS весь свод. Авторитетный прогон — в терминале владельца.

Self-Review

  • Покрытие спека: §D1 — Task A (resolveStateDir) + B (classifierContext router) + C (classifierContext reviewer); §D2 fail-safe — тесты resolveStateDir (пусто/не-строка → fallback+warned); §D3 крайние случаи — дефолт байт-в-байт + инъекция + null/undefined; критерий — Step 14.
  • Заглушек нет: каждый impl-шаг несёт полный код; тесты полные; команды vitest точные.
  • Согласованность имён: resolveStateDir / classifierContext единые; дефолты = точная копия текущих строк (backward-compat инвариант).
  • Стена: brain-config / router-classifier / brain-retro-opus-reviewer — НЕ discipline-source и НЕ normative → нормативный гейт не engage; правки проходят как обычные шаги запечатанного плана (стена М2 + content-floor + TDD-gate govern). Bash floor-safe (npx vitest). MultiEdit не используется (brain-retro — два отдельных Edit-шага 11/12). project_url_whitelist + wiring — вне scope.