#!/usr/bin/env node /** * door-coverage — авто-аудит покрытия дверей (P15-b): инструмент, не покрытый * matcher'ом верховного хука и не семя, = «забытая дверь» → флаг. Закрывает * пропущенный канал как КЛАСС, а не точечно (урок F1: PowerShell мимо Bash). */ export function auditDoors({ tools = [], matcher = [], seeds = [] }) { const star = matcher.includes('*'); const cover = new Set(matcher); const seed = new Set(seeds); const uncovered = star ? [] : tools.filter((t) => !cover.has(t) && !seed.has(t)); return { ok: uncovered.length === 0, uncovered }; } /** * Страховка к D (F): инструмент в «зелёном проходе» (seeds/observe), чья способность * мутирующая, = опасное исключение → флаг. isMutating инъектируется (общий критерий * способности с supreme-gate), чтобы не плодить зашитых списков. */ export function auditExempt({ exempt = [], isMutating }) { const flagged = exempt.filter((t) => isMutating(t)); return { ok: flagged.length === 0, flagged }; } /** Канонический набор мутирующих инструментов (по способности, выровнен с supreme-gate * default-deny: всё, что не observe-only и не seed). MCP-писатели динамические — не статичны. */ export const CANONICAL_MUTATING_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Skill']; export function isMutatingTool(name) { return CANONICAL_MUTATING_TOOLS.includes(name); } /** * Извлечь, какие инструменты покрывает matcher хука в settings.PreToolUse. * '*' → ['*'] (покрывает всё); "A|B" → ['A','B']; хук не найден / пустой matcher → [] * (пустой = аудит требует явного '*', иначе сигналим «дверь не покрыта»). */ export function extractGateMatcher(settings, hookBasename) { const pre = settings && settings.hooks && settings.hooks.PreToolUse; if (!Array.isArray(pre)) return []; for (const entry of pre) { const inner = entry && entry.hooks; if (!Array.isArray(inner)) continue; const has = inner.some((h) => h && typeof h.command === 'string' && h.command.includes(hookBasename)); if (!has) continue; const m = typeof entry.matcher === 'string' ? entry.matcher : ''; if (m === '*') return ['*']; return m.split('|').map((s) => s.trim()).filter(Boolean); } return []; }