2026-06-15 10:21:50 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* 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',
|
2026-06-15 13:17:28 +03:00
|
|
|
|
protected_paths: [],
|
2026-06-16 04:24:46 +03:00
|
|
|
|
tool_registry_path: 'docs/Tooling_v8_3.md',
|
2026-06-15 10:21:50 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** Наложить дефолты + 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));
|
|
|
|
|
|
}
|
2026-06-15 14:16:14 +03:00
|
|
|
|
|
|
|
|
|
|
/** 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 };
|
|
|
|
|
|
}
|