69 lines
4.9 KiB
Markdown
69 lines
4.9 KiB
Markdown
# Спека: Фаза 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 — вне этой спеки.
|
||
|
||
## Контракт {#D1}
|
||
|
||
Три обратно-совместимых дополнения (новый параметр / новая чистая функция, дефолт = текущее):
|
||
|
||
1. `tools/brain-config.mjs` — новая чистая функция `resolveStateDir(value)` → объект
|
||
`{ stateDir, warnedFallback }`. Непустая строка → `{ stateDir: <trim>, warnedFallback: false }`;
|
||
пусто / пробелы / не-строка → `{ stateDir: '.claude/brain-state', warnedFallback: true }`.
|
||
2. `tools/router-classifier.mjs` — `buildClassifierPromptStructured(userPrompt, registry,
|
||
{ enrichment = true, classifierContext = '<текущая строка>' } = {})`: строка проекта в `system`
|
||
строится из `classifierContext`. Дефолт = `CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)`
|
||
(байт-в-байт текущая).
|
||
3. `tools/brain-retro-opus-reviewer.mjs` — `buildReviewPromptStructured(episode,
|
||
{ classifierContext = 'Лидерра' } = {})`: имя проекта в первой строке `system` строится из
|
||
`classifierContext`. Дефолт = `Лидерра` (байт-в-байт текущая).
|
||
|
||
## fail-safe `state_dir` (§5.1) {#D2}
|
||
|
||
- Безопасное направление отказа: пустой/невалидный `state_dir` НЕ выключает наблюдателя/стоимость
|
||
молча — резолвер возвращает дефолт `.claude/brain-state` + `warnedFallback: true` (wiring потом
|
||
издаёт громкий warn и пишет в fallback, не делает тихий no-op).
|
||
- Резолвер чист (без I/O): определяет каталог + флаг fallback. Проверка записываемости каталога —
|
||
забота wiring (Task 7), не этого резолвера.
|
||
|
||
## Крайние случаи и критерий {#D3}
|
||
|
||
Крайние случаи (обязательны в тестах):
|
||
|
||
- `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, чистые функции, без
|
||
новых зависимостей); строковые дефолты — точная копия текущих захардкоженных строк.
|
||
|
||
```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"}
|
||
]
|
||
```
|