feat(brain-config): wire classifier_context в classify + reviewViaDirectApi (Task 7 follow-up #2)

Glue читает loadConfig().classifier_context; pure-fns и их тесты уже были. Дефолт = brain.local.md Лидерра-строка; greenfield без файла = generic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 07:25:51 +03:00
parent 13e080bea5
commit a16023743c
3 changed files with 105 additions and 1 deletions
@@ -0,0 +1,87 @@
# classifier_context wiring — findings + план (Task 7 follow-up #2)
**Дата:** 2026-06-16
**Статус:** investigation done, ожидает решения по способу исполнения (стена/TDD-gate)
**Контекст:** второй пункт остатка Фазы 1 (handoff №5 §6). `classifier_context` — последний
config-ключ без wiring (handoff §5 таблица ⏳).
---
## 1. Находка: pure-part уже сделан (Task 6)
Чистые функции уже принимают `classifierContext` параметром + тесты зелёные:
| Функция | Дефолт (хардкод) | Тест |
|---|---|---|
| `router-classifier.mjs:291` `buildClassifierPromptStructured(..., {classifierContext})` | `'CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)'` | `router-classifier.test.mjs:658-664` |
| `brain-retro-opus-reviewer.mjs:45` `buildReviewPromptStructured(ep, {classifierContext})` | `'Лидерра'` | `brain-retro-opus-reviewer.test.mjs:118-123` |
`brain-config.mjs:47` дефолт `classifier_context: 'generic project (no profile configured)'`;
`.claude/brain.local.md` = `CRM-проект «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)`.
## 2. Остаток: только caller-wiring (2 места)
Вызывающий код пока зовёт чистые функции БЕЗ `classifierContext` → используется хардкод-дефолт.
Нужно: читать `loadConfig().classifier_context` и передавать (паттерн Task 7 — `await import` + try/catch,
+ override через `options.classifierContext` для тестов/вызывающих).
### 2.1. router-classifier.mjs — `classify` (стр. 652, внутри `llmCall`-closure)
```javascript
let classifierContext = options.classifierContext;
if (classifierContext === undefined) {
try {
const { loadConfig } = await import('./brain-config.mjs');
classifierContext = loadConfig().classifier_context;
} catch { /* дефолт в buildClassifierPromptStructured */ }
}
const structured = buildClassifierPromptStructured(prompt, registry, {
enrichment: options.enrichment ?? true,
...(classifierContext !== undefined ? { classifierContext } : {}),
});
```
### 2.2. brain-retro-opus-reviewer.mjs — `reviewViaDirectApi` (стр. 141)
```javascript
let classifierContext = options.classifierContext;
if (classifierContext === undefined) {
try {
const { loadConfig } = await import('./brain-config.mjs');
classifierContext = loadConfig().classifier_context;
} catch { /* дефолт buildReviewPromptStructured */ }
}
const structured = buildReviewPromptStructured(
episode, classifierContext !== undefined ? { classifierContext } : {},
);
```
Legacy sync-обёртки (`buildClassifierPrompt:277`, `buildReviewPrompt:101`) — НЕ трогаем (не боевой путь,
sync — нельзя await; остаются на дефолте для тестов).
## 3. Backward-compat
`brain.local.md` value кормит ОБА промпта:
- классификатор: `Ты классификатор задач для CRM-проект «Лидерра» (...)` (было «проекта» — падеж).
- reviewer: `for the CRM-проект «Лидерра» (...) brain-governance experiment` (было короткое `Лидерра`).
Изменения формулировок минимальны, классификацию не меняют; проектный контекст остаётся Лидерра
(handoff «дефолт уже Лидерра»). Greenfield без brain.local.md → `generic project` (желаемое).
## 4. Стена / TDD-gate (способ исполнения — решает владелец)
Оба файла — production (`isProductionCodePath` true для `tools/*.mjs`). `enforce-tdd-gate` (PreToolUse
Edit/Write) требует свежий test-edit + vitest-**RED** в ходе; floor-escape его НЕ снимает (отдельный хук).
Полный/одиночный vitest через Claude-Bash рушит сборку (harness-collapse) → чистого RED не достать.
Это glue вокруг уже-протестированных pure-fns (новой логики нет) — gate over-fires. Варианты:
- **(a)** Владелец даёт TDD-gate override (`ремонт инфраструктуры` + `ремонт: <причина>`, или `срочно`) →
Edit-инструментом точно + floor-escape на стену. Надёжно для multi-line кода.
- **(b)** Владелец применяет в своём терминале (PowerShell `.Replace`) — multi-line код фрагильнее.
- **(c)** Отложить.
## 5. Верификация
Авторитетный свод — терминал владельца (`npx vitest run --config vitest.config.tools.mjs`): pure-fn тесты
зелёные, регрессий нет. Wiring-glue сам по себе unit-тестом не покрыт (I/O `await import`) — честно: покрыта
чистая функция, не склейка. Коммит — терминал владельца.
+10 -1
View File
@@ -138,7 +138,16 @@ export async function reviewViaDirectApi(episode, options = {}) {
const { callAnthropicAPI } = await import('./router-classifier.mjs');
const apiKey = options.apiKey ?? process.env.ROUTER_LLM_KEY;
if (!apiKey) return null;
const structured = buildReviewPromptStructured(episode);
let classifierContext = options.classifierContext;
if (classifierContext === undefined) {
try {
const { loadConfig } = await import('./brain-config.mjs');
classifierContext = loadConfig().classifier_context;
} catch { /* дефолт buildReviewPromptStructured */ }
}
const structured = buildReviewPromptStructured(
episode, classifierContext !== undefined ? { classifierContext } : {},
);
try {
const text = await callAnthropicAPI(structured, {
apiKey,
+8
View File
@@ -649,8 +649,16 @@ export async function classify(prompt, registry, options = {}) {
const llmCall = options.llmCall || (async ({ onMetrics } = {}) => {
const apiKey = process.env.ROUTER_LLM_KEY;
if (!apiKey) return null;
let classifierContext = options.classifierContext;
if (classifierContext === undefined) {
try {
const { loadConfig } = await import('./brain-config.mjs');
classifierContext = loadConfig().classifier_context;
} catch { /* дефолт в buildClassifierPromptStructured */ }
}
const structured = buildClassifierPromptStructured(prompt, registry, {
enrichment: options.enrichment ?? true,
...(classifierContext !== undefined ? { classifierContext } : {}),
});
const text = await callAnthropicAPI(structured, {
apiKey,