Files
brain/tools/brain-config.mjs
T

79 lines
3.2 KiB
JavaScript
Raw Normal View History

/**
* 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 };
}