# Переезд базы на управляемую 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, переносятся дампом как есть, нужен только 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 <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**
```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 <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**
```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 <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 без метки:
```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 <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: Проверить правила маскирования**
```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=<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 (код, без секретов)**
```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 <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-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). ✅
