200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
# Фаза 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.
|
||
|
||
```skills-json
|
||
["test-driven-development"]
|
||
```
|
||
|
||
```steps-json
|
||
[
|
||
{"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"}
|
||
]
|
||
```
|
||
|
||
```verified-context-json
|
||
[
|
||
{"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):
|
||
|
||
```javascript
|
||
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` = последние строки `loadConfig` — `return resolveConfig(parseBrainConfig(md));\n}`):
|
||
|
||
```javascript
|
||
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';` рядом):
|
||
|
||
```javascript
|
||
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).`):
|
||
|
||
```javascript
|
||
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` уже есть — иначе добавить):
|
||
|
||
```javascript
|
||
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) {`):
|
||
|
||
```javascript
|
||
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.',`):
|
||
|
||
```javascript
|
||
`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.
|