4.9 KiB
Спека: Фаза 1 config-seam — state_dir резолвер + classifier_context параметр
Дата: 2026-06-15 Статус: Draft (Фаза 1 config-seam, чистые pure-seam'ы из остатка Tasks 5/6) Канон: design v6 §3.3 / §5 / §5.1; план Фазы 1; project-brain-plugin-phase1-progress.
Цель
Закрыть два «чистых» config-seam ключа Фазы 1 на уровне pure-функций, не меняя поведение
claude-brain (backward-compat: дефолт = текущее значение), без подключения в main() (wiring —
отдельная задача): (1) fail-safe резолвер state_dir (§5.1) в brain-config; (2) параметр
classifier_context в двух prompt-builder'ах (router-classifier, brain-retro-opus-reviewer),
где сейчас захардкожена строка проекта «Лидерра».
NB: registry_path уже параметризован (loadRegistry({ registryPath })) — извлекать нечего.
project_url_whitelist (домены вплетены в regex) и wiring — вне этой спеки.
Контракт
Три обратно-совместимых дополнения (новый параметр / новая чистая функция, дефолт = текущее):
tools/brain-config.mjs— новая чистая функцияresolveStateDir(value)→ объект{ stateDir, warnedFallback }. Непустая строка →{ stateDir: <trim>, warnedFallback: false }; пусто / пробелы / не-строка →{ stateDir: '.claude/brain-state', warnedFallback: true }.tools/router-classifier.mjs—buildClassifierPromptStructured(userPrompt, registry, { enrichment = true, classifierContext = '<текущая строка>' } = {}): строка проекта вsystemстроится изclassifierContext. Дефолт =CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)(байт-в-байт текущая).tools/brain-retro-opus-reviewer.mjs—buildReviewPromptStructured(episode, { classifierContext = 'Лидерра' } = {}): имя проекта в первой строкеsystemстроится изclassifierContext. Дефолт =Лидерра(байт-в-байт текущая).
fail-safe state_dir (§5.1)
- Безопасное направление отказа: пустой/невалидный
state_dirНЕ выключает наблюдателя/стоимость молча — резолвер возвращает дефолт.claude/brain-state+warnedFallback: true(wiring потом издаёт громкий warn и пишет в fallback, не делает тихий no-op). - Резолвер чист (без I/O): определяет каталог + флаг fallback. Проверка записываемости каталога — забота wiring (Task 7), не этого резолвера.
Крайние случаи и критерий
Крайние случаи (обязательны в тестах):
resolveStateDir('docs/observer')→{ stateDir: 'docs/observer', warnedFallback: false }.resolveStateDir('')/' '/null/undefined→{ stateDir: '.claude/brain-state', warnedFallback: true }(без исключения).buildClassifierPromptStructured(p, reg)(без опции) →systemсодержит«Лидерра»(дефолт байт-в-байт); с{ classifierContext: 'X' }→systemсодержитX.buildReviewPromptStructured(ep)(без опции) →systemсодержитЛидерра; с{ classifierContext: 'X' }→ содержитX.
Критерий приёмки:
- Полный свод tools проходит:
npx vitest run --config vitest.config.tools.mjs(авторитетно — в терминале владельца; сессионный Bash рушит воркеры под нагрузкой). - Регрессия: ни один существующий тест не падает (дефолты сохранили поведение байт-в-байт).
Конвенция: имена resolveStateDir / classifierContext; стиль tools/ (ESM, чистые функции, без
новых зависимостей); строковые дефолты — точная копия текущих захардкоженных строк.
[
{"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"}
]