#!/usr/bin/env node /** * classify-destructive (§4, N1) — единый двухуровневый классификатор разрушительности * по СУТИ команды. floor — точный необратимый набор (hard-block пола). suspicious — * грубый набор для голосов судьи. Инвариант: floor ⇒ suspicious. Один источник правды * (Δ9-б: другого regex разрушительных команд в tools/ быть не должно). * * Parity (Step 1.0 audit-context): suspicious — надмножество двух прежних DESTRUCTIVE_RE * (judge-orchestrator:84 + router-engine:12). Источник 1 давал голый `migrate`+`drop table`, * источник 2 — `format`+`force-push`-литерал. Оба покрыты ниже. Standalone `-rf`-литерал * (over-broad FP источника 1, ловил `tar -rf`) намеренно НЕ перенесён — `rm -rf` покрыт * словом `rm` + rmIsFloor. * * M7 Task 1.1 (P-1): contentBlock переиспользует ЕДИНЫЙ матчер matchBashHardBlacklist из * shell-content-rules (тот же источник, что content-floor М5) — не ручная копия. */ import { matchBashHardBlacklist } from './shell-content-rules.mjs'; // F2: rm разрушителен (floor) только если есть И рекурсия (-r/-R/--recursive), И force // (-f/--force) — слитно (-rf), раздельно, короткими ИЛИ длинными флагами. Длинно-флаговый // `rm --recursive --force` так же необратим → floor (старый regex его терял). function rmIsFloor(cmd) { if (!/\brm\b/i.test(cmd)) return false; const rec = /-[a-z]*r/i.test(cmd) || /--recursive\b/i.test(cmd); const force = /-[a-z]*f/i.test(cmd) || /--force\b/i.test(cmd); return rec && force; } // Точный необратимый набор — то, что пол блокирует НАГЛУХО (rm — через rmIsFloor). const FLOOR_RE = [ // force-push/перепись. NB (sharp-edges Step 1.9): длинные флаги --force/--force-with-lease // БЕЗ обязательного \s перед ними — иначе `git push "--force"` (кавычки) обходил floor, // а shell кавычки снимает → реальный force-push. Выровнено с каноном shell-content-rules. // Короткий -f и refspec + требуют \s (чтобы не ловить подстроки/-rf). /\bgit\s+push\b[^\n]*(?:--force\b|--force-with-lease\b|\s-f\b|\s\+\S)/i, /\breset\s+--hard\b/i, /\bartisan\s+migrate:(?:fresh|reset|refresh)\b/i, /\bartisan\s+db:wipe\b/i, ]; // Грубый набор для судьи (надмножество floor) — лишний голос не вредит. // F1: +format (его содержал detectHighRisk М3 — иначе rewire потеряет триггер форматирования диска). // +force-push-литерал: источник 2 (router-engine:12) ловил его как отдельную альтернативу — // сохраняем строгую parity (синтетический токен, реальный `git push --force` покрыт `--force`). const SUSPICIOUS_RE = [ /\b(?:rm|rmdir|drop|delete|truncate|migrate|format)\b/i, /--force\b/i, /\bforce-push\b/i, /\breset\s+--hard\b/i, /\bartisan\s+migrate:(?:fresh|reset|refresh)\b/i, /\bartisan\s+db:wipe\b/i, ]; export function classifyDestructive(command) { const cmd = String(command || ''); const floor = rmIsFloor(cmd) || FLOOR_RE.some((re) => re.test(cmd)); // M7 Task 1.1 (правило 8 §4.1, V1): опасное-по-СОДЕРЖАНИЮ — единый источник (P-1), whole-string. // Это поле для видимости судьи; фактический блок пола идёт через bashIsContentBlock (whole+per-segment, Task 1.3). const contentBlock = matchBashHardBlacklist(cmd) !== null; // P-5: судья М4 (голоса) видит content-опасное как suspicious. const suspicious = floor || contentBlock || SUSPICIOUS_RE.some((re) => re.test(cmd)); const reason = floor ? 'необратимая команда (floor)' : contentBlock ? 'опасная по содержанию (content-block)' : suspicious ? 'подозрительная команда (suspicious)' : 'не разрушительная'; return { floor, suspicious, contentBlock, reason }; }