Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.
Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).
Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.
LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
28 KiB
Переезд базы на управляемую Yandex (Путь А) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Перевести боевую базу liderra.ru на Yandex Managed Service for PostgreSQL так, чтобы Яндекс сам делал копии/восстановление/отказоустойчивость, БЕЗ переписывания логики портала. Изоляцию клиентов и аудит сохраняем, переложив их с «особых прав ролей» (BYPASSRLS / session_replication_role, которых на управляемой базе нет) на штатные механизмы PostgreSQL (разрешающие RLS-политики + GUC-метка).
Architecture: Меняется СЛОЙ РОЛЕЙ/ПОЛИТИК БД, не код портала. Три служебные роли теряют атрибут BYPASSRLS → вместо него получают разрешающие политики FOR ALL TO <role> USING(true). Пересчёт аудит-цепочки перестаёт отключать триггеры через superuser-параметр → триггер пропускает себя по GUC-метке app.audit_rebuild. Маскировка ПДн переходит на встроенную в управляемую базу anon 1.3.2. Всё проверяется на тест-кластере (копии) до боевого переезда; данные мигрируют дамп/restore (база ~13 МБ → окно простоя минуты).
Tech Stack: PostgreSQL 16, Yandex Managed Service for PostgreSQL (роли mdb_superuser/mdb_admin, пулер Odyssey порт 6432, SSL verify-full + CA), Laravel 13 (config/database.php, Pest), pg_dump -Fc/pg_restore, anon 1.3.2 (built-in), rls-reviewer агент, prod-deploy-validator агент.
🔄 Обновлено 26.06.2026 под правки прод-сессии (проверено по коду до
7efe9e3e): схема v8.53→v8.55 (+2 SaaS-таблицыsupplier_deferred_sync,supplier_sync_runs— без RLS, переносятся дампом как есть, нужен только GRANTcrm_supplier_worker, уже в blanket-гранте). Дамп для переезда берём из текущего прода (там уже live разблокировка смены источника + маршрутизация по снимку).SharesSupplierPdo— тестовая механика, прогон тестов на тест-кластере (Phase 5) должен её учитывать. Деньги-инвариант — волатилен (см. правило 3).
⚠️ Правила выполнения (не нарушать)
- Вся переделка — сперва на ТЕСТ-КЛАСТЕРЕ (копии). Боевую базу не трогаем, пока изоляция и деньги не подтверждены на копии.
- Боевой переезд — только после явного «выкатывай» владельца +
prod-deploy-validator(GO). Помечено[PROD-GATE]. - Деньги-инвариант (ВОЛАТИЛЕН — сверять снимок ДО==ПОСЛЕ одной операции, НЕ с фиксированным числом): портал живой, числа растут каждый день. На 26.06.2026 прод =
tenant 2 ≈ 1 838 400 ₽ / 1016 сделок (990 живых + 26 удал.) / 999 731 лид(было 1 836 400 / 1013 на 25.06). Точный текущий запрос и значение фиксируются в Phase 0 непосредственно перед операцией; критерий — равенство снимков до и после, а не совпадение с историческим числом. - Изоляция: после смены модели — обязательный прогон
rls-reviewer+ тест «клиент A не видит данных клиента B» на копии. Любая утечка = СТОП. - Старую базу не гасим ≥7 дней после переезда (горячий откат: вернуть
DB_HOST).
📋 Для владельца — что делаем простым языком
- Берём у Яндекса управляемую базу — сначала тестовую копию, чтобы всё проверить, не трогая боевую.
- На копии меняем «замки»: служебные роли получают доступ через правила вместо особого права. Портал при этом не переписываем.
- Проверяем на копии, что клиенты не видят чужого и деньги целы.
- Налаживаем маскировку персональных данных встроенным средством Яндекса.
- В тихое окно (простой — минуты) переключаем боевой портал на управляемую базу. Старую держим неделю на всякий случай.
- Дальше Яндекс сам делает копии, запасную базу и переключение — ручного нянченья больше нет.
Что от вас нужно: в консоли Яндекса создать тестовый и боевой кластеры + 5 пользователей базы (могу провести по шагам или с доступом сделать сам). Детали — «Prerequisites».
Prerequisites (действия владельца в консоли Yandex Cloud)
- P1. Тест-кластер Managed PostgreSQL 16, 1 хост (network-ssd), БД
liderra, в той же сетиru-central1. Для проверок HA не нужен. - P2. 5 пользователей БД через консоль (БЕЗ возможности BYPASSRLS — её там и нет):
crm_app_user,crm_admin_user,crm_migrator,crm_audit_writer,crm_supplier_worker. Пароли — в Lockbox. - P3. CA-сертификат кластера (
https://storage.yandexcloud.net/cloud-certs/CA.pem) — для шифрованного подключения. - P4. Включить расширения на кластере (консоль/CLI):
pgcrypto,pg_trgm,btree_gin,pgaudit,anon(1.3.2) — через Shared preload libraries + список расширений БД. - P5. Боевой кластер Managed PostgreSQL 16, HA: ≥2 хоста (мастер + кворумная реплика), хранение копий 30 дней, PITR включён. Создаётся ПЕРЕД Phase 6, не раньше.
Пароли/ключи — в YC Lockbox, в
.envсервера иapp/.env.production; в git не попадают (gitleaks).
Phase 0 — Discovery + точный money-check
Task 0: Снять факты с боевого (read-only)
Files: заметка docs/superpowers/findings/2026-06-25-db-migration/phase0.md.
- Step 1: Зафиксировать money-check (точные таблица/колонка)
Run:
ssh liderra-prod "sudo -u postgres psql -d liderra -At -c \
\"SELECT 'balance', balance::text FROM tenants WHERE id=2
UNION ALL SELECT 'deals', count(*)::text FROM deals WHERE tenant_id=2;\""
Expected: число порядка balance|1838400.00, deals|1016 (волатильно — на 26.06; зафиксировать актуальное значение как эталон «ДО» для сверки «ПОСЛЕ»). Если баланс не в tenants — найти реальную таблицу (\d tenants), записать точный запрос как «money-check». Добавить также проверку лидов (999 731 на 26.06) и новых SaaS-таблиц supplier_deferred_sync/supplier_sync_runs (есть в схеме v8.55).
- Step 2: Список RLS-таблиц и держателей BYPASSRLS (для шва B)
Run:
ssh liderra-prod "sudo -u postgres psql -d liderra -At -c \
\"SELECT tablename FROM pg_tables WHERE schemaname='public' AND rowsecurity ORDER BY 1;\"; \
echo '---ROLES---'; sudo -u postgres psql -d liderra -At -c \
\"SELECT rolname FROM pg_roles WHERE rolbypassrls AND rolcanlogin ORDER BY 1;\""
Expected: ~36 таблиц; роли с BYPASSRLS = crm_admin_user, crm_migrator, crm_supplier_worker (подтвердить, что ровно эти три).
- Step 3: Объём базы + версия + anon для оценки переноса.
ssh liderra-prod "sudo -u postgres psql -d liderra -At -c \
\"select pg_size_pretty(pg_database_size('liderra'));\"; \
sudo -u postgres psql -d liderra -At -c \"select default_version from pg_available_extensions where name='anon';\""
- Step 4: Commit (docs-only)
git add docs/superpowers/findings/2026-06-25-db-migration/phase0.md
git commit -m "docs(db-migration): снимок боевого перед Путь А (Phase 0)"
Phase 1 — Модель безопасности на тест-кластере (швы A + B)
Цель: на копии воспроизвести изоляцию БЕЗ BYPASSRLS и доказать, что она держит.
Task 1: Залить схему+данные на тест-кластер
Files: runbook deploy/db-migration/01-load-test-cluster.md.
- Step 1: Снять свежий дамп боевого (read-only):
ssh liderra-prod "sudo -u postgres pg_dump -Fc -d liderra -f /tmp/liderra-$(date -u +%Y%m%d).dump" && \
ssh liderra-prod "cat /tmp/liderra-*.dump" > /tmp/liderra.dump
- Step 2: Restore в тест-кластер под владельцем (
mdb_superuser-пользователь кластера):
pg_restore --no-owner --no-privileges --no-acl \
-h <TEST_FQDN> -p 6432 -U <admin_user> -d liderra \
"sslmode=verify-full sslrootcert=~/.postgresql/root.crt" /tmp/liderra.dump
-
Step 3: Money-check на тест-кластере — точный запрос из Task 0. Expected:
1836400.00 / 1013. Расхождение → разобраться до продолжения. -
Step 4: Commit runbook
git add deploy/db-migration/01-load-test-cluster.md
git commit -m "docs(db-migration): загрузка тест-кластера из дампа боевого"
Task 2: Разрешающие политики для служебных ролей (замена BYPASSRLS)
Files:
-
Create:
db/migrations/2026_06_26_service_role_bypass_policies.sql -
Create:
app/database/migrations/2026_06_26_000001_service_role_bypass_policies.php(обёртка, выполняет .sql) -
Test:
app/tests/Feature/Db/ServiceRoleBypassPolicyTest.php -
Step 1: Написать падающий тест изоляции/обхода
ServiceRoleBypassPolicyTest.php — два утверждения: (1) crm_app_user с tenant-контекстом видит только своего; (2) служебная роль (эмуляция: разрешающая политика присутствует) видит cross-tenant. На тест-БД (sqlite/pg) проверяем через наличие политики srv_bypass:
public function test_service_bypass_policies_exist_on_all_rls_tables(): void
{
$rls = DB::select("SELECT tablename FROM pg_tables WHERE schemaname='public' AND rowsecurity");
foreach ($rls as $t) {
$pol = DB::select(
"SELECT 1 FROM pg_policies WHERE tablename = ? AND policyname = 'srv_bypass'",
[$t->tablename]
);
$this->assertNotEmpty($pol, "Нет srv_bypass на {$t->tablename}");
}
}
-
Step 2: Прогнать — упадёт (политик ещё нет). Run:
composer test -- --filter=ServiceRoleBypassPolicyTestExpected: FAIL. -
Step 3: Написать SQL-миграцию (идемпотентную)
db/migrations/2026_06_26_service_role_bypass_policies.sql:
-- Заменяет атрибут BYPASSRLS на разрешающие политики для 3 служебных ролей.
-- crm_app_user НЕ получает политику → остаётся tenant-isolated.
-- crm_audit_writer НЕ получает (append-only, без bypass).
DO $$
DECLARE t record;
BEGIN
FOR t IN SELECT tablename FROM pg_tables
WHERE schemaname='public' AND rowsecurity
LOOP
EXECUTE format('DROP POLICY IF EXISTS srv_bypass ON public.%I;', t.tablename);
EXECUTE format(
'CREATE POLICY srv_bypass ON public.%I AS PERMISSIVE FOR ALL
TO crm_admin_user, crm_migrator, crm_supplier_worker
USING (true) WITH CHECK (true);', t.tablename);
END LOOP;
END $$;
Laravel-обёртка 2026_06_26_000001_service_role_bypass_policies.php — DB::unprepared(file_get_contents(base_path('../db/migrations/2026_06_26_service_role_bypass_policies.sql'))) + запись в db/CHANGELOG_schema.md.
- Step 4: Применить на тест-кластер + прогнать тест — PASS
psql -h <TEST_FQDN> -p 6432 -U <admin_user> -d liderra -f db/migrations/2026_06_26_service_role_bypass_policies.sql
composer test -- --filter=ServiceRoleBypassPolicyTest
Expected: политики на всех ~36 таблицах; тест PASS.
-
Step 5: Применить GRANT'ы (адаптированный 02_grants под владельца) + сменить пароли/роли на тест-кластере (
db/02_grants.sql, запуск подmdb_superuser-пользователем вместо postgres). -
Step 6: Commit
git add db/migrations/2026_06_26_service_role_bypass_policies.sql app/database/migrations/2026_06_26_000001_service_role_bypass_policies.php app/tests/Feature/Db/ServiceRoleBypassPolicyTest.php db/CHANGELOG_schema.md
git commit -m "feat(db): разрешающие политики служебных ролей вместо BYPASSRLS (Managed PG)"
Task 3: Проверка изоляции на тест-кластере (живая)
- Step 1: Подключиться как
crm_app_user(не-bypass) к тест-кластеру, поставить контекст tenant 2, проверить, что видны ТОЛЬКО его сделки:
psql -h <TEST_FQDN> -p 6432 -U crm_app_user -d liderra "sslmode=verify-full sslrootcert=~/.postgresql/root.crt" \
-c "BEGIN; SET LOCAL app.current_tenant_id = 2; SELECT count(*) FROM deals; \
SET LOCAL app.current_tenant_id = 999; SELECT count(*) FROM deals; ROLLBACK;"
Expected: для tenant 2 — 1013; для несуществующего 999 — 0. (Изоляция держит без BYPASSRLS.)
-
Step 2: Подключиться как
crm_supplier_worker— убедиться, что cross-tenant виден (разрешающая политика работает):SELECT count(DISTINCT tenant_id) FROM deals;> 1. -
Step 3: Прогнать
rls-reviewerна схему с новыми политиками. Вердикт без orphan/leak. Если замечания — исправить и повторить Task 2.
Phase 2 — Пересчёт аудита без session_replication_role (шов C, TDD)
Task 4: GUC-метка вместо отключения триггеров
Files:
-
Modify:
db/schema.sql(функцияaudit_block_mutation, ~строка 3320) -
Modify:
app/app/Console/Commands/AuditRebuildChain.php:107,137 -
Test:
app/tests/Feature/AuditRebuildChainTest.php -
Step 1: Написать падающий тест — пересчёт цепочки в партиции проходит без
session_replication_role, при выставленномapp.audit_rebuild='on', и аудит остаётся append-only без метки:
public function test_rebuild_allowed_only_with_guc_flag(): void
{
// без флага — UPDATE log_hash блокируется триггером
$this->expectException(\Illuminate\Database\QueryException::class);
DB::statement("UPDATE activity_log SET log_hash = log_hash WHERE id = (SELECT min(id) FROM activity_log)");
}
public function test_rebuild_passes_with_guc(): void
{
DB::transaction(function () {
DB::statement("SET LOCAL app.audit_rebuild = 'on'");
DB::statement("UPDATE activity_log SET log_hash = log_hash WHERE id = (SELECT min(id) FROM activity_log)");
});
$this->assertTrue(true); // не упало
}
-
Step 2: Прогнать — упадёт (триггер пока знает только session_replication_role). Run:
composer test -- --filter=AuditRebuildChainTestExpected: FAIL наtest_rebuild_passes_with_guc(триггер блокирует UPDATE). -
Step 3: Изменить функцию
audit_block_mutation()(вdb/schema.sql+ миграция):
CREATE OR REPLACE FUNCTION audit_block_mutation() RETURNS trigger AS $$
BEGIN
-- Разрешить пересчёт цепочки доверенным процессом по метке (вместо superuser-параметра).
IF current_setting('app.audit_rebuild', true) = 'on'
AND current_user IN ('crm_migrator','crm_admin_user') THEN
RETURN NEW;
END IF;
RAISE EXCEPTION 'audit table is append-only (%.%)', TG_TABLE_SCHEMA, TG_TABLE_NAME;
END;
$$ LANGUAGE plpgsql;
Запись в db/CHANGELOG_schema.md.
-
Step 4: Изменить
AuditRebuildChain.php— обернуть пересчёт вDB::connection('pgsql_supplier')->transaction(...)и внутриSET LOCAL app.audit_rebuild = 'on'вместо строк 107/137 (session_replication_role). Убрать сброс на'origin'(SET LOCAL сам сбрасывается в конце транзакции; это ещё и Odyssey-safe). -
Step 5: Прогнать — PASS +
composer pint && composer stan. Run:composer test -- --filter=AuditRebuildChainTestExpected: PASS оба теста. -
Step 6: Commit
git add db/schema.sql app/app/Console/Commands/AuditRebuildChain.php app/tests/Feature/AuditRebuildChainTest.php db/CHANGELOG_schema.md
git commit -m "fix(audit): пересчёт цепочки через GUC-метку app.audit_rebuild (без superuser)"
Историческая миграция
2026_05_23_hole2_partition_audit_tables.sql:34(тожеsession_replication_role) при переезде дамп/restore НЕ перезапускается — данные переносятся как есть. Future-proof правка той миграции — follow-up, не блокер.
Phase 3 — Маскировка ПДн на встроенной anon (шов D)
Task 5: Применить метки маскировки на тест-кластере
Files: db/anon_masking_labels.sql (метки уже написаны), runbook deploy/db-migration/03-masking.md.
- Step 1: Включить anon на кластере (Prerequisite P4) и применить метки:
psql -h <TEST_FQDN> -p 6432 -U <admin_user> -d liderra -c "LOAD 'anon';" -f db/anon_masking_labels.sql
Если какая-то функция отсутствует в 1.3.2 — заменить на доступный аналог (partial, partial_email, fake_first_name/last_name, MASKED WITH VALUE NULL — стандартные, ожидаются присутствующими).
- Step 2: Проверить правила маскирования
psql ... -c "SELECT relname, attname, masking_function FROM anon.pg_masking_rules ORDER BY 1,2;"
Expected: ~16 правил (users/deals/supplier_leads/pd_*/auth_log/tenants), как в anon_masking_labels.sql.
-
Step 3: Тест маскированного дампа — снять дамп ролью с меткой MASKED, убедиться, что телефоны/почта замаскированы, а
tenant_id/ключи целы; money-check на восстановленном из маскированного дампа НЕ обязан совпадать по ПДн, но числа сделок целы. -
Step 4: Commit runbook
git add deploy/db-migration/03-masking.md
git commit -m "docs(db-migration): маскировка ПДн на встроенной anon (тест-кластер)"
Phase 4 — Подключение приложения (шов: SSL + Odyssey)
Task 6: Конфиг подключения к управляемой базе
Files: Modify: app/config/database.php (опции SSL), app/.env.production (значения).
- Step 1: Написать падающий тест конфига —
pgsqlconnection отдаётsslmode=verify-fullиsslrootcert, когда заданы env:
public function test_pgsql_uses_verify_full_when_configured(): void
{
config(['database.connections.pgsql.sslmode' => 'verify-full',
'database.connections.pgsql.sslrootcert' => '/etc/liderra/yc-ca.pem']);
$this->assertSame('verify-full', config('database.connections.pgsql.sslmode'));
$this->assertNotNull(config('database.connections.pgsql.sslrootcert'));
}
-
Step 2: Прогнать — упадёт (нет ключа
sslrootcert). Run:composer test -- --filter=pgsql_uses_verify_fullExpected: FAIL. -
Step 3: Добавить опции в
$pgsqlConnection(config/database.php:22):
'sslmode' => env('DB_SSLMODE', 'prefer'),
'sslrootcert' => env('DB_SSLROOTCERT'), // путь к CA.pem управляемой базы
(pgsql_supplier наследует через array_merge — править не нужно.)
-
Step 4: Прогнать — PASS. Run:
composer test -- --filter=pgsql_uses_verify_fullExpected: PASS. -
Step 5: Подготовить
.env.productionзначения (НЕ коммитить секреты):DB_HOST=<FQDN кластера>,DB_PORT=6432(Odyssey),DB_SSLMODE=verify-full,DB_SSLROOTCERT=/etc/liderra/yc-ca.pem,DB_SUPPLIER_USERNAME=crm_supplier_worker. Положить CA на сервер. -
Step 6: Commit (код, без секретов)
git add app/config/database.php
git commit -m "feat(db): SSL verify-full + CA для подключения к управляемой базе"
Phase 5 — Полный прогон на копии (генеральная репетиция)
Task 7: Прогнать портал против тест-кластера
- Step 1: Поднять приложение (локально/стейдж) с
.env, указывающим на тест-кластер (port 6432, verify-full). - Step 2: Регрессия —
composer test(Pest, backend+billing+RLS), Vitest. Expected: зелено (RLS/изоляция/деньги). - Step 3: Живой смоук — вход клиентом, открыть сделки (видит свои), приём тестового лида через
pgsql_supplier-путь (cross-tenant запись проходит), пересчёт аудитаaudit:rebuild-chainна партиции (работает через GUC),audit:verify-chainsintact. - Step 4: Money-check на тест-кластере:
1836400.00 / 1013. - Step 5: Зафиксировать результат репетиции в
docs/superpowers/findings/2026-06-25-db-migration/dry-run.md+ commit (docs-only).
Phase 6 — Боевой переезд [PROD-GATE]
Task 8: Переключение боевого на управляемый кластер (окно, простой минуты)
Files: runbook deploy/db-migration/06-cutover.md.
- Step 1: Запросить GO —
prod-deploy-validator+ явное «выкатывай». NO-GO → стоп. - Step 2: Создать боевой кластер (Prerequisite P5, HA ≥2 хоста), накатить роли/политики/grants/маскировку как на тесте (всё уже отлажено).
- Step 3: Окно обслуживания — включить maintenance на портале (очередь на паузу).
- Step 4: Финальный дамп боевого → restore в боевой кластер:
ssh liderra-prod "sudo -u postgres pg_dump -Fc -d liderra" > /tmp/final.dump
pg_restore --no-owner --no-privileges -h <PROD_FQDN> -p 6432 -U <admin_user> -d liderra "sslmode=verify-full sslrootcert=..." /tmp/final.dump
- Step 5: Применить политики
srv_bypass+ grants + метки маскировки на боевом кластере. - Step 6: Money-check на новом боевом кластере —
1836400.00 / 1013. Расхождение → НЕ переключать, разобраться. - Step 7: Переключить
app/.envна сервере:DB_HOST=<PROD_FQDN>,DB_PORT=6432, SSL verify-full, CA.php artisan config:cache(от www-data, квирк 107), снять maintenance, перезапустить очередь. - Step 8: Verify — HTTP 200 (главная+
/login), вход клиентом видит свои сделки (изоляция), приём тестового лида, очередь активна, 0 свежих ошибок,audit:verify-chainsintact, money-check ещё раз. - Step 9: Commit runbook + обновить
ПИЛОТ.md(по команде «обнови пилот»): база = управляемая, копии/HA — Яндекс.
Phase 7 — Завершение
Task 9: Декомиссия и нормативка
- Step 1: Старую самоуправляемую базу держать выключенной как откат ≥7 дней, затем погасить (по команде владельца).
- Step 2: Снять с сервера старый cron бэкапов/реплику Пути Б — больше не нужны (копии у Яндекса). Скрипты
deploy/db-backup/*пометить устаревшими. - Step 3: Синк нормативки (
normative-syncагент):00_create_roles.sql(модель без BYPASSRLS), CLAUDE.md §2 (база = Managed PG, без BYPASSRLS), память. Через положенные каналы (CLAUDE.md — плагин). - Step 4: Закрыть в памяти переход (Путь А done).
Self-Review (выполнено автором плана)
- Покрытие швов A–F (assessment §3a): A,B → Task 2; C → Task 4; D → Task 5; E → Task 2 Step 5; F (partition DDL) — наследует роль-владельца кластера, отдельной правки кода не требует (DDL уже через
pgsql_supplier/migrator). ✅ - App-код не переписываем: подтверждено — меняются только
config/database.php(SSL, Task 6) иAuditRebuildChain.php(Task 4). ~30 джобов наpgsql_supplierи ~50 местapp.current_tenant_id— НЕ трогаются. ✅ - Изоляция проверена дважды: Task 3 (живой тест app_user vs supplier_worker) + Task 7 Step 2 (regression RLS) +
rls-reviewer. ✅ - Деньги-инвариант в Task 0/1/5/7/8 (1 836 400.00 / 1013). ✅
- Прод-гейты на всех боевых шагах (Phase 6). ✅
- Имена-консистентность: политика
srv_bypass, меткаapp.audit_rebuild, 5 ролей, порт 6432 — единообразны во всех задачах. ✅ - Открытое (честно): точная таблица баланса и парность функций anon 1.3.2 подтверждаются в Phase 0/3 на копии, не выдуманы. Историческая миграция с
session_replication_role— follow-up (не блокер при дамп/restore). ✅