diff --git a/.claude/hooks/prod-db-guard.mjs b/.claude/hooks/prod-db-guard.mjs index 172931cf..85b05354 100644 --- a/.claude/hooks/prod-db-guard.mjs +++ b/.claude/hooks/prod-db-guard.mjs @@ -10,6 +10,34 @@ // Боевая база = Managed PG кластер c9q2cvtjpq3hgq6l0r96 (rw-endpoint *.mdb.yandexcloud.net). // Тест-база = отдельная liderra_testing (её сносить можно). // +// Что считается сносом (проверено живым вызовом, сторож проверок — tools/prod-db-guard.test.mjs): +// 1. yc managed-postgresql cluster delete — и короткие имена yc mdb pg / yc mdb postgresql +// 2. yc managed-postgresql database delete — и те же короткие имена +// 3. SQL DROP DATABASE +// 4. штатная программа dropdb (стоит на этой машине: C:\Program Files\PostgreSQL\16\bin\dropdb.exe) +// 5. SQL DROP SCHEMA … CASCADE — базу оставляет, всё содержимое выносит +// 6. снос средствами самого приложения: artisan db:wipe / migrate:fresh / migrate:refresh +// Пункты 4-5 и короткие имена добавлены 05.08.2026: до этого они проходили насквозь. +// Пункт 6 добавлен 07.08.2026 (смена 32, по слову владельца) — см. ниже. +// +// 🟢 ДВЕ ДЫРЫ ИЗ ЗАМЕРА М-400 ЗАКРЫТЫ 07.08.2026. Прежде здесь стояло примечание +// «чего сторож не видит», и оно было верным: обе дыры замерены живым вызовом смены 23. +// +// 🔴 ПЕРВАЯ БЫЛА ДВОЙНОЙ. `php artisan db:wipe --force` не числился разрушительной командой +// ВООБЩЕ — хотя выносит из базы все таблицы разом. Значит даже с прямо названной боевой +// базой этот способ сноса сторожу был неизвестен. Туда же `migrate:fresh` и `migrate:refresh`: +// оба сперва сносят, и только потом наливают заново. +// +// 🔴 ВТОРАЯ: снос виден словами, а цель спрятана — ни имени базы, ни кластера, ни хоста +// в тексте нет, всё в переменной (`psql "$PROD_URL" -c "DROP SCHEMA public CASCADE"`). +// 🔑 Отличить по тексту боевую связь от тестовой НЕЛЬЗЯ — значит решать надо не «какая это +// база», а «названа ли она вообще». Разрушительная команда со спрятанной целью теперь +// останавливается: назови цель словами либо поставь маркер владельца. +// +// 🔴 ЦЕНА ЭТОЙ СТРОГОСТИ, НАЗВАННАЯ ЧЕСТНО: снос ТЕСТОВОЙ базы по связи из переменной +// тоже остановится — сторож не может знать, что под переменной тестовая. Это плата за то, +// что боевую нельзя снести молча. Обе двери владельца работают и здесь. +// // Override владельца: маркер `PROD-DESTROY-OK` в самой команде ИЛИ env ALLOW_PROD_DB_DESTROY=1. import { readFileSync } from 'node:fs'; @@ -39,21 +67,74 @@ const targetsProd = /\bc-[a-z0-9]+\.(rw|ro)\.mdb\.yandexcloud\.net/i.test(cmd) || /\bliderra\b(?!_)/i.test(cmd); +// Имя команды PostgreSQL у `yc` пишется ТРЕМЯ способами: полным именем и двумя +// короткими алиасами. Замерено живым вызовом 05.08.2026: сторож знал только полное, +// и `yc mdb pg cluster delete --id c9q2cvtjpq3hgq6l0r96` проходил насквозь. +const YC_PG = String.raw`(?:managed-postgresql|mdb\s+(?:postgresql|postgres|pg))`; + // Деструктив над управляемой БД/кластером. -const clusterDelete = /managed-postgresql\s+cluster\s+delete/i.test(cmd); // снос кластера — всегда катастрофа -const databaseDelete = /managed-postgresql\s+database\s+delete/i.test(cmd); // снос управляемой БД -const dropDatabase = /\bdrop\s+database\b/i.test(cmd); // SQL DROP DATABASE +const clusterDelete = new RegExp(`${YC_PG}\\s+cluster\\s+delete`, 'i').test(cmd); // снос кластера — всегда катастрофа +const databaseDelete = new RegExp(`${YC_PG}\\s+database\\s+delete`, 'i').test(cmd); // снос управляемой БД +const dropDatabase = /\bdrop\s+database\b/i.test(cmd); // SQL DROP DATABASE -const destructive = clusterDelete || databaseDelete || dropDatabase; +// Штатная программа PostgreSQL `dropdb` — сносит базу так же насмерть, как SQL DROP DATABASE, +// и она СТОИТ на этой машине (C:\Program Files\PostgreSQL\16\bin\dropdb.exe). Ловим имя +// программы в любом виде: голым словом, с расширением, полным путём (слэши любые, кавычки). +// 🪤 Границы руками, а не `\b`: `\b` не отличает `dropdb` от `--dropdb`, а путь `bin\dropdb.exe` +// должен попадаться. Сноса ТЕСТОВОЙ базы это не касается — ниже решает targetsProd. +const dropdbTool = /(?