feat(brain-config): чистый загрузчик brain.local.md + fail-safe resolveConfig (Фаза 1 Задачи 1-2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-15 10:21:50 +03:00
parent 3a422a00c3
commit b9730afb8a
4 changed files with 281 additions and 0 deletions
@@ -0,0 +1,52 @@
# План-церемония v3: фикс сбора тестов `brain-config` (Фаза 1, Задачи 1–2)
## Цель
Исправить сбор тестов модуля `tools/brain-config.mjs`. Конфиг `vitest.config.tools.mjs`
задаёт `globals: true` (верифицировано — см. `verified-context-json`, якорь
`vitest-globals`), значит `describe/it/expect` доступны как глобалы и **не должны**
импортироваться явно. Текущий тест-файл импортирует их из `'vitest'`, из-за чего
`describe` приходит из контекста без раннера и сбор падает («reading config»).
Фикс: переписать тест без этого импорта (опора на глобалы конфига). Код модуля уже
корректен и не трогается. Контракт и крайние случаи — спека
`2026-06-15-brain-config-module-spec.md` (якоря D1–D6). Итог: единичный файл и
полный свод `tools/` зелёные.
## Безопасность отката (премортем)
- **Глобалы гарантированы.** `globals: true` в конфиге (верифицировано якорем) —
значит `describe/it/expect` инъектируются раннером глобально; тест без импорта
их получит. Если бы глобалов не было, этот же тест падал бы по `describe is not
defined` — но конфиг подтверждает обратное.
- **Нет необратимых действий.** План — только Write тест-файла + два прогона тестов.
Ни `git commit`, ни `push`, ни CI не запускаются → денежных/CI-последствий нет.
- **Откат тривиален.** Трогается один файл (`tools/brain-config.test.mjs`); модуль
`brain-config.mjs` не меняется. Если шаг 2 неожиданно RED — план встаёт до шага 3
(регрессии), откат = восстановить прежний тест-файл, другие файлы не затронуты.
```skills-json
["test-driven-development"]
```
```steps-json
[
{"op":"Write","object":"tools/brain-config.test.mjs","ref":"D6"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs tools/brain-config.test.mjs","ref":"D6"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D6"}
]
```
## Порядок шагов (человекочитаемо)
1. **Write** `tools/brain-config.test.mjs` — те же тесты `parseBrainConfig` +
`resolveConfig` fail-safe (D1/D2/D3), но **без** `import ... from 'vitest'`
(глобалы из конфига, `globals: true`). Критерий — D6.
2. **Bash** vitest на файл — ожидается GREEN (сбор починен, контракт + fail-safe).
3. **Bash** полный свод `tools/` — ожидается GREEN (регрессия не сломана, модуль новый).
```verified-context-json
[
{"id":"tools-pure-module-style","kind":"EXTRACTED","ref":"tools/cost-pricing.mjs","anchor":"export const PRICING = Object.freeze("},
{"id":"vitest-globals","kind":"EXTRACTED","ref":"vitest.config.tools.mjs","anchor":"globals: true"}
]
```
@@ -0,0 +1,92 @@
# Спека: модуль `tools/brain-config.mjs` — чтение проектной настройки мозга
**Дата:** 2026-06-15
**Статус:** к исполнению (Фаза 1, Задачи 1–2)
## Цель
Дать движку единый чистый модуль, который читает проектную настройку из файла
`.claude/brain.local.md` и отдаёт типизированные значения с безопасными
направлениями отказа. Захардкоженные «знания про конкретный проект», рассыпанные
по `tools/*.mjs`, должны получить один источник правды. Этот модуль — только
загрузчик и нормализатор настройки; он не меняет поведение существующих файлов
(они подключатся к нему отдельными задачами с дефолтами = текущие значения).
## Контракт API {#D1}
Модуль `tools/brain-config.mjs` (Node ESM, стиль каталога `tools/`) экспортирует
три чистые/полу-чистые функции:
- `parseBrainConfig(md) -> object` — чистая. На вход markdown-строка (содержимое
`.claude/brain.local.md`). Возвращает объект ключей из YAML-frontmatter. На `null`,
пустую строку или текст без frontmatter — пустой объект `{}`.
- `resolveConfig(raw) -> object` — чистая. На вход результат `parseBrainConfig`.
Накладывает дефолты и fail-safe направления (§D3). Возвращает полный объект
настройки.
- `loadConfig(root = '.', fsImpl = fs) -> object` — I/O-обёртка. Читает
`${root}/.claude/brain.local.md`; если файла нет — отдаёт дефолты (через
`resolveConfig({})`). `fsImpl` инъектируется для тестов.
Ключи настройки: `config_version`, `registry_path`, `state_dir`, `evidence_archive`,
`normative_files` (список), `project_url_whitelist` (список), `classifier_context`,
`economy_default`, `enabled_hook_groups` (список).
## Алгоритм парсинга frontmatter {#D2}
Минимальный YAML-frontmatter без внешних зависимостей:
- Границы frontmatter — регэксп `^---\n([\s\S]*?)\n---` (блок между первыми `---`).
- Внутри блока построчно:
- строка вида ` - item` (элемент списка) добавляется к текущему ключу-списку;
- строка вида `key: value` начинает новый ключ; пустое `value` означает начало
списка (ключ инициализируется пустым массивом, последующие `- item` в него
падают);
- непустое числовое `value` (`^\d+$`) приводится к числу, иначе остаётся строкой;
- после скалярного ключа «текущий ключ-список» сбрасывается.
- Строки, не подходящие ни под один шаблон, игнорируются.
## Fail-safe направления (дефолты + отказ) {#D3}
`resolveConfig` накладывает безопасные дефолты на отсутствующие ключи. Отсутствие
или невалидность ключа НЕ отключает молча безопасность.
| Ключ | Дефолт при отсутствии | Направление |
|---|---|---|
| `state_dir` | `.claude/brain-state` | безопасный дефолт |
| `evidence_archive` | `brain-state` | безопасный дефолт |
| `normative_files` | `[]` (легитимно пусто) | явная пустота |
| `registry_path` | `''` (зависимости off осознанно) | явная пустота |
| `project_url_whitelist` | `[]` + флаг `project_url_whitelist_failClosed: true` | **fail-closed** (внешнее закрыто) |
| `classifier_context` | строка с маркером `generic` | безопасная деградация качества |
| `economy_default` | `'100'` | безопасный дефолт |
`project_url_whitelist_failClosed` равен `true`, когда whitelist пуст или не массив
(потребитель обязан блокировать внешние адреса), и `false`, когда список непуст.
## Крайние случаи {#D4}
- `parseBrainConfig(null)` и `parseBrainConfig('')``{}`.
- Текст без frontmatter (`'тело без ---'`) → `{}`.
- `loadConfig` при отсутствующем файле (читалка бросает) → дефолты, без падения.
- Числовое значение (`config_version: 1`) → число `1`, не строка.
- Список из одного элемента → массив длины 1.
## Конвенция заголовка модуля {#D5}
Файл начинается с shebang `#!/usr/bin/env node` и JSDoc-блока, описывающего
назначение (единый источник проектной настройки мозга, чистый парсер +
`resolveConfig` с fail-safe дефолтами). Стиль — как у соседних чистых модулей
каталога `tools/` (замороженные константы через `Object.freeze`, ESM-импорт fs).
## Критерий приёмки {#D6}
- `npx vitest run --config vitest.config.tools.mjs tools/brain-config.test.mjs`
сначала RED (нет реализации), затем GREEN (все кейсы контракта и fail-safe).
- `npx vitest run --config vitest.config.tools.mjs` — полный свод `tools/` остаётся
зелёным (модуль новый, существующее поведение не затронуто).
```verified-context-json
[
{"id":"tools-pure-module-style","kind":"EXTRACTED","ref":"tools/cost-pricing.mjs","anchor":"export const PRICING = Object.freeze("}
]
```