Files
portal/docs/superpowers/plans/2026-06-25-db-migration-path-a-managed.md
T
Дмитрий 347bc3a13b feat(db): Путь А — пересчёт аудита через GUC + политики srv_bypass вместо BYPASSRLS
Шов 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>
2026-06-26 09:39:19 +03:00

28 KiB
Raw Blame History

Переезд базы на управляемую 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:

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=ServiceRoleBypassPolicyTest Expected: 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.phpDB::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=AuditRebuildChainTest Expected: 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=AuditRebuildChainTest Expected: 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: Написать падающий тест конфигаpgsql connection отдаёт 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_full Expected: 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_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 (код, без секретов)

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: Запросить GOprod-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-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 (выполнено автором плана)

  • Покрытие швов AF (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).