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:
@@ -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("}
|
||||
]
|
||||
```
|
||||
Reference in New Issue
Block a user