Files
brain/docs/superpowers/specs/2026-06-15-brain-config-module-spec.md
T

6.3 KiB
Raw Blame History

Спека: модуль tools/brain-config.mjs — чтение проектной настройки мозга

Дата: 2026-06-15 Статус: к исполнению (Фаза 1, Задачи 1–2)

Цель

Дать движку единый чистый модуль, который читает проектную настройку из файла .claude/brain.local.md и отдаёт типизированные значения с безопасными направлениями отказа. Захардкоженные «знания про конкретный проект», рассыпанные по tools/*.mjs, должны получить один источник правды. Этот модуль — только загрузчик и нормализатор настройки; он не меняет поведение существующих файлов (они подключатся к нему отдельными задачами с дефолтами = текущие значения).

Контракт API

Модуль 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

Минимальный YAML-frontmatter без внешних зависимостей:

  • Границы frontmatter — регэксп ^---\n([\s\S]*?)\n--- (блок между первыми ---).
  • Внутри блока построчно:
    • строка вида - item (элемент списка) добавляется к текущему ключу-списку;
    • строка вида key: value начинает новый ключ; пустое value означает начало списка (ключ инициализируется пустым массивом, последующие - item в него падают);
    • непустое числовое value (^\d+$) приводится к числу, иначе остаётся строкой;
    • после скалярного ключа «текущий ключ-список» сбрасывается.
  • Строки, не подходящие ни под один шаблон, игнорируются.

Fail-safe направления (дефолты + отказ)

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, когда список непуст.

Крайние случаи

  • parseBrainConfig(null) и parseBrainConfig(''){}.
  • Текст без frontmatter ('тело без ---') → {}.
  • loadConfig при отсутствующем файле (читалка бросает) → дефолты, без падения.
  • Числовое значение (config_version: 1) → число 1, не строка.
  • Список из одного элемента → массив длины 1.

Конвенция заголовка модуля

Файл начинается с shebang #!/usr/bin/env node и JSDoc-блока, описывающего назначение (единый источник проектной настройки мозга, чистый парсер + resolveConfig с fail-safe дефолтами). Стиль — как у соседних чистых модулей каталога tools/ (замороженные константы через Object.freeze, ESM-импорт fs).

Критерий приёмки

  • 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/ остаётся зелёным (модуль новый, существующее поведение не затронуто).
[
  {"id":"tools-pure-module-style","kind":"EXTRACTED","ref":"tools/cost-pricing.mjs","anchor":"export const PRICING = Object.freeze("}
]