88aa122cf8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.2 KiB
JavaScript
78 lines
3.2 KiB
JavaScript
/**
|
||
* 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: [],
|
||
});
|
||
|
||
/** Наложить дефолты + 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 };
|
||
}
|