# Переезд базы на управляемую 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 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, переносятся дампом как есть, нужен только GRANT `crm_supplier_worker`, уже в blanket-гранте). Дамп для переезда берём из **текущего** прода (там уже live разблокировка смены источника + маршрутизация по снимку). `SharesSupplierPdo` — тестовая механика, прогон тестов на тест-кластере (Phase 5) должен её учитывать. Деньги-инвариант — волатилен (см. правило 3). ## ⚠️ Правила выполнения (не нарушать) 1. **Вся переделка — сперва на ТЕСТ-КЛАСТЕРЕ (копии).** Боевую базу не трогаем, пока изоляция и деньги не подтверждены на копии. 2. **Боевой переезд — только после явного «выкатывай» владельца** + `prod-deploy-validator` (GO). Помечено `[PROD-GATE]`. 3. **Деньги-инвариант (ВОЛАТИЛЕН — сверять снимок ДО==ПОСЛЕ одной операции, НЕ с фиксированным числом):** портал живой, числа растут каждый день. На 26.06.2026 прод = `tenant 2 ≈ 1 838 400 ₽ / 1016 сделок (990 живых + 26 удал.) / 999 731 лид` (было 1 836 400 / 1013 на 25.06). Точный текущий запрос и значение фиксируются в Phase 0 непосредственно перед операцией; критерий — равенство снимков до и после, а не совпадение с историческим числом. 4. **Изоляция:** после смены модели — обязательный прогон `rls-reviewer` + тест «клиент A не видит данных клиента B» на копии. Любая утечка = СТОП. 5. **Старую базу не гасим** ≥7 дней после переезда (горячий откат: вернуть `DB_HOST`). --- ## 📋 Для владельца — что делаем простым языком 1. Берём у Яндекса **управляемую базу** — сначала **тестовую копию**, чтобы всё проверить, не трогая боевую. 2. На копии меняем «замки»: служебные роли получают доступ через правила вместо особого права. Портал при этом не переписываем. 3. Проверяем на копии, что **клиенты не видят чужого** и **деньги целы**. 4. Налаживаем маскировку персональных данных встроенным средством Яндекса. 5. В тихое окно (простой — минуты) **переключаем боевой портал** на управляемую базу. Старую держим неделю на всякий случай. 6. Дальше Яндекс сам делает копии, запасную базу и переключение — **ручного нянченья больше нет**. **Что от вас нужно:** в консоли Яндекса создать тестовый и боевой кластеры + 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: ```bash 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: ```bash 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** для оценки переноса. ```bash 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)** ```bash 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): ```bash 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`-пользователь кластера): ```bash pg_restore --no-owner --no-privileges --no-acl \ -h -p 6432 -U -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** ```bash 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`: ```php 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=ServiceRoleBypassPolicyTest` Expected: FAIL. - [ ] **Step 3: Написать SQL-миграцию (идемпотентную)** `db/migrations/2026_06_26_service_role_bypass_policies.sql`: ```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** ```bash psql -h -p 6432 -U -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** ```bash 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, проверить, что видны ТОЛЬКО его сделки: ```bash psql -h -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 без метки: ```php 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=AuditRebuildChainTest` Expected: FAIL на `test_rebuild_passes_with_guc` (триггер блокирует UPDATE). - [ ] **Step 3: Изменить функцию `audit_block_mutation()`** (в `db/schema.sql` + миграция): ```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=AuditRebuildChainTest` Expected: PASS оба теста. - [ ] **Step 6: Commit** ```bash 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) и применить метки: ```bash psql -h -p 6432 -U -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: Проверить правила маскирования** ```bash 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** ```bash 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: Написать падающий тест конфига** — `pgsql` connection отдаёт `sslmode=verify-full` и `sslrootcert`, когда заданы env: ```php 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_full` Expected: FAIL. - [ ] **Step 3: Добавить опции в `$pgsqlConnection`** (`config/database.php:22`): ```php '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_full` Expected: PASS. - [ ] **Step 5: Подготовить `.env.production` значения** (НЕ коммитить секреты): `DB_HOST=`, `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 (код, без секретов)** ```bash 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-chains` intact. - [ ] **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 в боевой кластер**: ```bash ssh liderra-prod "sudo -u postgres pg_dump -Fc -d liderra" > /tmp/final.dump pg_restore --no-owner --no-privileges -h -p 6432 -U -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=`, `DB_PORT=6432`, SSL verify-full, CA. `php artisan config:cache` (от www-data, квирк 107), снять maintenance, перезапустить очередь. - [ ] **Step 8: Verify** — HTTP 200 (главная+`/login`), вход клиентом видит свои сделки (изоляция), приём тестового лида, очередь активна, 0 свежих ошибок, `audit:verify-chains` intact, 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). ✅