Files
brain/tools/l1-watcher.mjs
T
2026-06-16 04:24:46 +03:00

87 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const PLUGIN_NAME_PATTERN = /[a-z][\w-]*(?:@[\w-]+)?/g;
export function parseAliases(raw) {
if (!raw) return {};
const out = {};
for (const line of raw.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 1) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (key && value) out[key] = value;
}
return out;
}
// Detects the L1 pattern (ADR-011 §6.1): a plugin enabled in settings.json
// that has no formalization in Tooling Прил. Н. Only the settings→Tooling
// direction is checked — the reverse ("documented but disabled") cannot be
// computed reliably because Tooling lists tools by human/group name while
// settings.json keys are machine IDs (`name@marketplace`).
export function detectDrift(settings, toolingText, aliases = {}) {
const enabled = Object.entries(settings.enabledPlugins || {})
.filter(([, v]) => v === true)
.map(([k]) => k);
const found = new Set();
for (const m of toolingText.matchAll(PLUGIN_NAME_PATTERN)) found.add(m[0]);
const missingInTooling = enabled.filter((p) => {
if (found.has(p)) return false;
const alias = aliases[p];
if (alias && toolingText.includes(alias)) return false;
return true;
});
return { missingInTooling };
}
function loadFileMaybe(path) {
try {
return existsSync(path) ? readFileSync(path, 'utf-8') : null;
} catch {
return null;
}
}
export function loadInputs(projectRoot = process.cwd(), toolRegistryPath = 'docs/Tooling_v8_3.md') {
const userSettings = JSON.parse(loadFileMaybe(join(homedir(), '.claude', 'settings.json')) || '{}');
const projectSettings = JSON.parse(loadFileMaybe(join(projectRoot, '.claude', 'settings.json')) || '{}');
const merged = {
enabledPlugins: { ...(userSettings.enabledPlugins || {}), ...(projectSettings.enabledPlugins || {}) },
};
const toolingRaw = toolRegistryPath ? loadFileMaybe(join(projectRoot, toolRegistryPath)) : null;
const tooling = toolingRaw || '';
const toolingPresent = toolingRaw !== null;
const aliasesRaw = loadFileMaybe(join(projectRoot, 'tools', '.l1-watcher-aliases.txt'));
const aliases = parseAliases(aliasesRaw);
return { settings: merged, tooling, aliases, toolingPresent };
}
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/l1-watcher.mjs')) {
let toolRegistryPath = 'docs/Tooling_v8_3.md';
try {
const { loadConfig } = await import('./brain-config.mjs');
toolRegistryPath = loadConfig().tool_registry_path;
} catch { /* дефолт */ }
const { settings, tooling, aliases, toolingPresent } = loadInputs(process.cwd(), toolRegistryPath);
if (!toolingPresent) {
console.log('[l1-watcher] OK — справочник инструментов не задан/не найден (skip)');
process.exit(0);
}
const drift = detectDrift(settings, tooling, aliases);
if (drift.missingInTooling.length > 0) {
console.error(`[l1-watcher] FAIL — plugins in settings but not formalized in Tooling Прил. Н:`);
drift.missingInTooling.forEach((p) => console.error(` - ${p}`));
console.error(`Run /claude-md-management:claude-md-improver to formalize.`);
console.error(`If the plugin is referenced in Tooling under a group/human name, add an alias to tools/.l1-watcher-aliases.txt.`);
process.exit(1);
}
console.log(`[l1-watcher] OK — 0 drift`);
process.exit(0);
}