/** * brain-config — единый источник проектной настройки мозга (.claude/brain.local.md). * * Чистый парсер frontmatter (parseBrainConfig) + resolveConfig с fail-safe * дефолтами (спека 2026-06-15-brain-config-module-spec.md §D3) + I/O-обёртка * loadConfig. Захардкоженные «знания про конкретный проект» из tools/*.mjs * получают один источник правды; направления отказа безопасны (отсутствие ключа * не отключает молча безопасность/деньги/защиту). */ import fsDefault from 'node:fs'; const FM_RE = /^---\n([\s\S]*?)\n---/; /** Минимальный YAML-frontmatter: `key: value` + список ` - item`. Без зависимостей. Чистая. */ export function parseBrainConfig(md) { const m = FM_RE.exec(String(md || '')); if (!m) return {}; const out = {}; let curKey = null; for (const line of m[1].split('\n')) { const item = /^\s+-\s+(.*)$/.exec(line); if (item && curKey) { (out[curKey] ||= []).push(item[1].trim()); continue; } const kv = /^([A-Za-z_]+):\s*(.*)$/.exec(line); if (!kv) continue; const [, k, v] = kv; if (v === '') { out[k] = []; curKey = k; } else { out[k] = /^\d+$/.test(v) ? Number(v) : v; curKey = null; } } return out; } /** Безопасные дефолты (§D3). Отсутствие ключа не отключает молча защиту/деньги. */ const DEFAULTS = Object.freeze({ state_dir: '.claude/brain-state', evidence_archive: 'brain-state', normative_files: [], registry_path: '', project_url_whitelist: [], classifier_context: 'generic project (no profile configured)', economy_default: '100', protected_paths: [], tool_registry_path: 'docs/Tooling_v8_3.md', }); /** Наложить дефолты + fail-safe направления (§D3). Чистая. */ export function resolveConfig(raw) { const c = { ...DEFAULTS, ...(raw || {}) }; // project_url_whitelist: пусто/не-массив → fail-CLOSED (внешка закрыта), не «пускать всё». c.project_url_whitelist_failClosed = !(Array.isArray(c.project_url_whitelist) && c.project_url_whitelist.length > 0); return c; } /** I/O-обёртка: прочитать .claude/brain.local.md проекта (нет файла → дефолты). */ export function loadConfig(root = '.', fsImpl = fsDefault) { let md = ''; try { md = fsImpl.readFileSync(`${root}/.claude/brain.local.md`, 'utf8'); } catch { md = ''; } return resolveConfig(parseBrainConfig(md)); } /** fail-safe резолвер state_dir (§5.1): непустая строка → как есть; иначе → безопасный дефолт * .claude/brain-state + warnedFallback (НЕ тихий no-op — wiring издаёт warn и пишет в fallback). */ export function resolveStateDir(value) { const v = typeof value === 'string' ? value.trim() : ''; if (v.length > 0) return { stateDir: v, warnedFallback: false }; return { stateDir: '.claude/brain-state', warnedFallback: true }; }