krona-staff/tests/e2e/NOTES-FOR-EXECUTORS.md
grok bf80b5040e docs(e2e): REPORT/NOTES — d52ecf3, b022885 и исправление сценария
Почему: таблица приёмки и разбор C6 должны соответствовать текущим SHA
веток Codex, а владелец должен видеть, какие расхождения с C6 остались
после d52ecf3 и какая часть красного была ошибкой сценария (типы счётчиков,
d477dde), а не продукта.

- REPORT: таблица по повторному прогону (b022885, d52ecf3), разбор d52ecf3.
- NOTES C6: что закрыл d52ecf3; открыто — многострочный JSON, сумма журнала
  в dry-run, dry-run по умолчанию и обязательные --apply/--actor/--backup-evidence.
- NOTES разд. 8: K18 на b022885 — ротация на ПК есть, на сервере нет,
  личные пути в новых файлах.
2026-09-26 17:20:23 +07:00

38 KiB
Raw Blame History

Заметки для исполнителей (Codex / Antigravity)

Автор: Grok (стенд тестов и независимая приёмка). Я не меняю src/, apps/, migrations/, deploy/; всё, что требует изменения продукта, описано здесь.

Как запускать:

# bootstrap-админ PostgreSQL 17 (только создаёт роли и БД; сервер и миграции под ним не работают)
export E2E_PG_ADMIN_URL=postgres://<admin>:<pass>@127.0.0.1:55433/postgres
node scripts/test/run-e2e.mjs                    # храповик known-defects (как в CI)
node scripts/test/run-e2e.mjs --no-known-defects # строгий режим: все дефекты красные
E2E_MIGRATIONS=roundtrip node scripts/test/run-e2e.mjs  # + откат всех миграций перед запуском
./scripts/test/ci-local.sh                       # локальный эквивалент verify/format/e2e

Правило храповика. Исправили дефект → его сценарий позеленел → run-e2e.mjs падает с «дефект исправлен, обновите known-defects.json» — удалите ID из tests/e2e/known-defects.json в том же коммите. Новый красный сценарий, которого нет в списке, — регрессия.

1. Предполагаемый контракт для решений владельца (OWN-01..11) — Codex, этап 4

API пока нет, поэтому сценарии tests/e2e/30-owner-decisions.e2e.ts написаны против наиболее вероятного контракта. Если выбираете другой — поменяйте тесты и этот раздел в том же PR, смысл проверок (кто что может и когда) менять нельзя. Все пути относительно /api/v1/staff.

C1. Роли точки/бухгалтера/директора (standalone-аналог Core RBAC)

  • POST /access-grants (только главный менеджер, staff:employee:write) тело { "role": "point_manager" | "accountant" | "director", "org_unit_ids": ["<ULID>"], "display_name": "..." } → 201 { "id", "role", "org_unit_ids", "access_token" }. Токен показывается один раз, хранится как хеш (как ключи сотрудников), используется как Authorization: Bearer.
  • point_manager видит/считает только свои org_unit_ids; чужая точка → 403 (и для нового POST /payroll/batches, и для старого POST /payroll/calculate).
  • В platform-режиме те же роли приходят из Core (User ≠ Employee, AGENTS.md).

C2. Пакет зарплаты точки и согласование

Метод и путь Кто Результат
POST /payroll/batches {org_unit_id, from, to} accountant; point_manager своей точки 201 {id, status:"draft", org_unit_id, lines:[{work_session_id, employee_id, org_unit_id, amount_minor}]}
POST /payroll/batches/:id/submit только accountant 200 {status:"submitted"}; point_manager → 403; повтор после подписи → 409
GET /notifications любой пользователь 200 {notifications:[{type:"payroll.batch.submitted", subject_id:<batchId>}]}; каждому директору ровно одно уведомление на отправку
POST /payroll/batches/:id/sign только director 200 {status:"signed"}; accountant → 403; если смена пакета изменилась после отправки → 409, пакет получает status:"returned"
POST /payroll/batches/:id/return {comment} director 200 {status:"returned", return_comment}
GET /payroll/batches/:id accountant, director 200 {status, return_comment, lines:[...]}

Инварианты, которые проверяют тесты:

  • до подписи в staff_payroll_ledger для сотрудников пакета нет ни accrual, ни payout (OWN-05);
  • подпись атомарно пишет accrual и payout; после подписи PATCH /work-sessions/:id для смен пакета → 409, пакет не отправляется повторно (OWN-06);
  • строка пакета принадлежит точке, где отработана смена (staff_work_sessions.org_unit_id), а не основной точке сотрудника (OWN-09).

C3. Согласие на биометрию

  • PUT /employees/:id/biometric-consent { "status": "granted" | "revoked", "document_ref": "..." } (главный менеджер) → 200.
  • GET /terminal/employees/:id/enrollment (терминальный токен) → 200 { "employee_id", "registration_allowed": boolean, "reason": "consent_missing" | "consent_revoked" | null }. Без записи о согласии — false/consent_missing (OWN-10); после отзыва — false/consent_revoked (OWN-11).
  • Шаблоны отпечатков на сервер не передаются (docs/TERMINAL-API-REQUIREMENTS.md); сервер хранит только факт согласия. Тот же флаг стоит отдать в /sync/pull (employee payload).

Сейчас все OWN красные с 404 Route ... not found (C1–C3 не реализованы); OWN-03..09 дополнительно упрутся в K3/N2 (подтверждение смены падает 500).

ID в названиях тестов — двойные «владелец/мой» ([K3/N2], [D6/OWN-01], [K17/OWN-10], [K19/OWN-07]), по таблице TASK-GROK-2 §2 и AUDIT.md.

C4. Проверочный пересчёт подписанной ведомости (K8/D1)

  • GET /payroll/runs/:id/verify (право staff:payroll:read) → 200 { "run_id", "stored_total_minor", "recalculated_total_minor", "matches": boolean }.
  • Пересчёт идёт по снимку версий правил, параметров и ставок, действовавших при подписи (и ревизиям смен), а не по текущим значениям. После смены ставки matches остаётся true.

C5. Параметры пресета в назначении оплаты (K9/D3)

  • PUT /employees/:id/pay-assignment принимает params: { <key>: integer }, ключи — из param_declarations выбранного правила (например, shift_rate_minor, percent_bp, upsell_rate_minor, avg_check_target_minor, avg_check_bonus_minor у waiter_upsell); неизвестный ключ → 400. GET .../pay-assignment возвращает их в params.
  • Старые поля rate_minor/percent_bp/guarantee_minor можно оставить как сокращения.

C6. Перенос арендатора пилота (PILOT-REHOME, TASK-CODEX-2 п.2.2)

Что проверяет tests/e2e/60-pilot-rehome.e2e.ts ([PILOT-REHOME]):

  • Запуск: DATABASE_ADMIN_URL=<админ БД> node scripts/rehome-tenant.mjs --from 01J00000000000000000000000 --to 01M2R6AC5652NPT3MG34DKN4ZP [--dry-run] [--json]. Стенд передаёт DATABASE_ADMIN_URL суперпользователя в БД стенда (E2E_DB_ADMIN_URL или E2E_PG_ADMIN_URL с именем БД).
  • --json: последней строкой stdout — {"tables":{"<таблица>":<строк у --from>},"ledger_amount_minor_sum":<число или строка>} (счётчики — число или строка с числом). Таблицы с нулём можно не печатать.
  • --dry-run: exit 0, те же числа, в БД ничего не меняется.
  • Перенос: exit 0; под --from 0 строк во всех таблицах с tenant_id (список из information_schema.columns, не вручную); у --to те же счётчики и та же SUM(amount_minor) в staff_payroll_ledger; другие арендаторы не тронуты.
  • В staff_audit_log у нового арендатора появляется запись с action, содержащим rehome (например, tenant.rehomed, старый tenant — в new_values).
  • После переноса krona_staff_app видит тех же сотрудников (те же id) под новым tenant и никого под старым; UPDATE staff_payroll_ledger ролью приложения по-прежнему отклоняется.
  • Повторный запуск (под --from пусто / у --to уже есть данные) — exit ≠ 0, ничего не меняет.

Что нужно учесть (видно по схеме, pg_policy/pg_constraint на PG17):

  • У staff_payroll_ledger нет политики UPDATE/DELETE, у приложения нет права UPDATE, FORCE RLS действует и на владельца: krona_migrator строки журнала не перенесёт. Нужна роль с обходом RLS (суперпользователь/BYPASSRLS) — поэтому DATABASE_ADMIN_URL.
  • Внешние ключи составные (tenant_id, …) и не DEFERRABLE: переносить родителя раньше детей нельзя — нужен порядок, отложенные проверки или session_replication_role = replica в одной транзакции.
  • Политики INSERT staff_audit_log, staff_payroll_ledger, staff_attendance_events, staff_timesheet_entries требуют actor_id/created_by/updated_by = current_setting('app.user_id') — для записи аудита без обхода RLS выставить app.user_id.

Расхождение с codex/fix-tenant-rehome (c73bb3b, 16:48 NOVT; ACCEPTANCE-3 с просьбой держаться C6 — 16:35). Сценарий на ветке: dry-run exit: Tenant rehome stopped. Unknown option --from. По существу скрипт делает всё, что проверяет сценарий (см. REPORT «Круг 3б»); не совпадает только интерфейс:

C6 (сценарий) c73bb3b Итог в сценарии
--from, --to --source, --target Unknown option --from, exit 1
без флага — перенос нужен --apply, без режима — Specify --dry-run or --apply. —
— --apply требует --actor <ULID> и --backup-evidence <json> у сценария нет ни того, ни другого
--json неизвестный флаг → exit 1; JSON печатается всегда —
DATABASE_ADMIN_URL DATABASE_REHOME_URL (запасной вариант DATABASE_MIGRATION_URL) —
одна строка JSON, числа многострочный JSON (JSON.stringify(…, null, 2)), счётчики — строки парсер сценария ищет JSON в последней строке
ledger_amount_minor_sum в dry-run и apply в dry-run суммы нет; в apply payrollLedger: [{currency, amountMinor}] с адаптером: expected 'undefined' to be '328228'
tables — строки у --from в apply tables — у --to после переноса (staff_audit_log +1) не проверяется

d52ecf3 (17:02) закрыл строки 1, 4, 5 таблицы: --from/--to, --json, DATABASE_ADMIN_URL принимаются. Без флага теперь dry-run (по C6 — перенос), так что запуск сценария без --apply не переносит данные. Открытыми остаются многострочный JSON (сценарий: dry-run без JSON-отчёта), сумма журнала в dry-run и обязательные --apply --actor --backup-evidence (разд. 6.3 в report-evidence/round3-k18-and-rehome.md). Типы счётчиков расходились из-за ошибки сценария; она исправлена в d477dde, сравнение идёт через Number().

Какой интерфейс оставить, решает владелец. Варианты: (а) Codex добавляет в скрипт C6-совместимые --from/--to/--json, DATABASE_ADMIN_URL, сумму журнала в dry-run, а --actor/--backup-evidence берёт из переменных окружения (например, REHOME_ACTOR_ID, REHOME_BACKUP_EVIDENCE); (б) владелец утверждает интерфейс c73bb3b — тогда я меняю сценарий под него отдельным коммитом, а сумму журнала в dry-run оставляю требованием (TASK-GROK-3 §4 п.3: «--dry-run — ничего не меняет, печатает те же числа», то есть числа п.2, включая SUM(amount_minor)). В TASK-CODEX-2 §2.2 п.4 dry-run описан как «только отчёт о количестве строк по таблицам», то есть по этому пункту c73bb3b задание выполняет, а расходится только с C6.

2. Дефекты продукта, найденные сценариями (все красные сейчас)

Команда и фактический вывод — tests/e2e/evidence/e2e-run.txt; таблица — tests/e2e/REPORT.md.

ID Файл Что исправить
K4/N1 src/portal.ts POST /employees 42P08 inconsistent types deduced for parameter $2: явные $2::varchar(26), $5::text или отдельный запрос профессии
K3/N2 src/portal.ts PATCH /work-sessions, src/domain/payroll/service.ts calculate/approve staff_outbox.occurred_at NULL (23502): передавать occurred_at или DEFAULT now() новой миграцией
K5/N4 service.ts после N2 проверить: пересекающийся период не должен начислять смену повторно (409 или исключение уже начисленных смен)
K12/N3 push-подписки allowlist push-сервисов / запрет приватных адресов, redirect:'manual'; сейчас сервер подключается к 127.0.0.1
N9 push-dispatcher.ts ON CONFLICT ... DO UPDATE SET employee_id → 409 для чужого endpoint
N10 GET /professions, /org-units требовать право справочника; терминал сейчас получает 200
N11 импорт валидировать kind и схему items → 400 вместо 500 (23514)
N12 PUT pay-assignment additionalProperties:false, верхние границы сумм
N12-DATE POST /payroll/calculate month=2026-13 → 500 22008; проверять месяц 01–12 → 400 (диапазон 2026-02-30 уже даёт 400)
K6/D5 portal.ts PATCH /work-sessions смена подписанной ведомости: 409, либо сторно в ledger; сейчас (на fix-live-database) 200 и начисление остаётся
K7/D2 operations.ts импорт payroll_entries дельта по сумме всех записей дня, вставка по ключу: две точки за день → 222,00 вместо 333,00 ₽; считать дельту по ключу
K8/D1 расчёт/подпись снимок правил и ставок в подписанной ведомости; контракт проверки C4 (ниже)
K9/D3 PUT pay-assignment сохранять params по param_declarations правила (C5 ниже); сейчас shift_rate_minor официанта теряется
S9 миграция 0011 FK устройств ON DELETE SET NULL на составном (tenant_id, org_unit_id) обнуляет tenant_id → 23502; SET NULL (org_unit_id) или RESTRICT
A4 обработчик ошибок в теле 5xx нет SQLSTATE (code) и текста PostgreSQL; подробности только в лог
K1/F2 config.ts/identity.ts standalone без токенов не должен выдавать права (процесс без токенов отдаёт 200 на /portal, /payroll, POST /professions)
K10/F3 POST/DELETE /session в cookie — идентификатор сессии, а не сырой токен; logout отзывает сессию
K11/F4 вход rate limit (тест ждёт 429 не позже 30 неверных попыток; идёт на отдельном процессе, чтобы лимит не мешал другим сценариям)
F7 POST /attendance/events использовать Idempotency-Key / staff_idempotency_keys
F10 assertSameOrigin мутации по cookie без Origin отклонять
K13/SYNC-A, K13/SYNC-B src/domain/sync/service.ts устройство не должно выставлять confirmed/approved (сейчас applied)
SYNC-C0 pull() bootstrap SELECT ... status FROM staff_org_units — такого столбца нет → 500 на любой начальной загрузке
S5/SYNC-C pull() bootstrap при has_more=true отдаётся next_cursor=latest, остаток после 100 сущностей теряется; нужен курсор снимка
S5/SYNC-D портал/импорт изменения вне sync не пишутся в staff_sync_change_log → терминал не узнаёт о новых/изменённых сотрудниках
S4/SYNC-E executeCommand проверять status='active' сотрудника
S4/SYNC-F executeCommand сохранять org_unit_id в staff_attendance_events; payload не может переопределить точку устройства
S3/SYNC-G push() ошибки БД одной команды → rejected для неё, а не 500 и откат всего пакета
S8/SYNC-H /sync/devices assertCookieMutationOrigin как в портале
K1/SYNC-I requireManagerContext без токенов не выдавать права (как F2)
S2/SYNC-J executeCommand окно времени отметки как в POST /attendance/events (−180 дней … +5 мин)
SYNC-K acknowledge() отклонять курсор больше MAX(cursor_seq)

3. deploy/ (K1/PILOT-1, K2/PILOT-2) — Codex

  • PILOT-1: все подстановки в deploy/pilot/compose.yaml → ${VAR:?VAR is required} (сейчас 14 без :?: пустой STANDALONE_MANAGER_TOKEN молча превращает pilot в F2).
  • PILOT-2: NODE_ENV: development в app. Конфликт: config.ts запрещает STAFF_RUNTIME_MODE=standalone при NODE_ENV=production. Нужно решение: либо разрешить standalone в production при обязательных токенах и HTTPS, либо отдельный режим для pilot. Тест требует только «не development».
  • N6: scripts/create-app-role.mjs не работает мигратором без SUPERUSER («Only roles with SUPERUSER may change the SUPERUSER attribute»). CI и e2e обходятся scripts/test/provision-pg.mjs (роли создаёт bootstrap-админ, как docker/postgres/init-app-role.sh). В pilot мигратор = POSTGRES_USER = суперпользователь; рекомендую схему как в provision-pg.mjs.

4. CI

  • Задаче docker-build нужен раннер с меткой docker-host (Docker daemon). Без него задача ждёт в очереди; остальные задачи не блокируются.
  • format красная, пока не исправлен F14 (23 файла на замер 2026-09-26, в основном src/, docs/, tests/unit/).
  • format станет зелёной после влития codex/fix-format-baseline (проверено на ветке).
  • Задача windows выключена, пока нет раннера с меткой windows: включается переменной репозитория KRONA_WINDOWS_RUNNER=true; для полного e2e нужен секрет WINDOWS_E2E_PG_ADMIN_URL (локальная PostgreSQL 17 на раннере).
  • tests/e2e/evidence/ не хранится в git; в CI это артефакт e2e-evidence.
  • Реальный Forgejo-раннер мной не запускался (нет API-токена) — не проверено.

5. N13 (гонка первого расчёта) и утечка текста ошибок БД

  • scripts/test/n13-race.mjs, вывод tests/e2e/evidence/n13-race.txt: 10/25/50 параллельных первых POST /payroll/calculate × 5 попыток. Как есть — все 500 (N2), гонку не видно. В одноразовой БД с обходом N2 (DEFAULT now() только там) — 0 из 30 некорректных попыток (без смен и с подтверждённой сменой): один run_id, один расчёт в БД.
  • Почему не воспроизводится (вывод из кода, не проверено отдельным экспериментом): calculatePayroll сначала вызывает ensureDefaultPayRules, а там INSERT ... ON CONFLICT DO UPDATE по одним и тем же строкам staff_pay_rules тенанта. Первая транзакция держит блокировки этих строк до коммита, остальные ждут — расчёты одного тенанта выполняются по очереди случайно. Если upsert правил вынесут из расчёта или сделают DO NOTHING, окно SELECT ... FOR UPDATE (строки нет) → INSERT ... ON CONFLICT DO NOTHING со своим runId → строки/UPDATE по несуществующему runId вернётся. Рекомендация: явный pg_advisory_xact_lock по (tenant, org_unit, period_start) или INSERT ... ON CONFLICT DO UPDATE ... RETURNING id. Скрипт стоит перезапустить после такого изменения.
  • Ответы 500 содержат сырой текст ошибки PostgreSQL (например null value in column "occurred_at" of relation "staff_outbox" ...) — клиенту видна схема БД. Для 5xx отдавать нейтральное сообщение, подробности — только в лог.

6. Приёмка веток исполнителей (2026-09-26)

Подробно — tests/e2e/REPORT.md и tests/e2e/report-evidence/acceptance-*.txt.

  • codex/fix-live-database (ea68e12, перепроверено на b289dd6): K3/N2 и K4/N1 зелёные на живой PostgreSQL 17. Дальше по цепочке открылись K5/N4 (196800 вместо 98400 — двойное начисление) и K6/D5 (отклонение оплаченной смены: 200, начисление осталось). A4 частично: POST /employees больше не 500, но month=2026-13 по-прежнему отдаёт code=22008 с текстом.
  • codex/fix-format-baseline (fd1ec3d): prettier --check . чистый. Но коммит «style» содержит функциональное исправление Antigravity: src/domain/sync/service.ts и tests/unit/sync.test.ts после нормализации prettier совпадают с antigravity/fix-reject-device-shifts (0 строк diff) и отличаются от 331e490 на 255/139 строк. Изменение поведения стоит вынести в отдельный коммит с объяснением или влить ветку Antigravity отдельно, иначе история смешивает темы (AGENTS.md).
  • antigravity/fix-reject-device-shifts (795bba0, перепроверено на 1df2019 — уже на f5ac179): K13/SYNC-A и K13/SYNC-B зелёные, остальное без изменений.
  • Ни одна ветка не влита в основную (f5ac179), поэтому ID из known-defects.json не удалены.
  • Суммы в тестах кратны рублю: расчёт округляет строку до rounding_unit_minor (по умолчанию 100 коп.). Если округление должно быть другим — это решение продукта, не теста.

7. Круг 3 (TASK-GROK-3, 2026-09-26)

Команды и полный вывод — tests/e2e/REPORT.md («Круг 3») и tests/e2e/report-evidence/round3-*.md. Приёмка одной командой: node scripts/test/accept-branch.mjs <ветка>… (нужен E2E_PG_ADMIN_URL или готовые роли в E2E_MIGRATOR_URL + E2E_APP_URL).

  • Antigravity, sync-bootstrap.test.ts (8baad55): Error: Called end on pool more than once при Tests 2 passed. Причина: app.close() в тесте вызывает хук onClose → database.close() (src/app.ts:148), затем afterAll закрывает тот же Database ещё раз. На текущей голове 24cc18d файл зелёный (1 passed (1) | 2 passed (2)).
  • Antigravity, sync-rejection.test.ts (1df2019): column "created_by" of relation "staff_employees" does not exist в beforeAll (Tests 2 skipped). На 6c5bf36/b283736 исправлено.
  • Конфликты: на прежних головах 1df2019 × 8baad55 и 8baad55 × 513cdff конфликтовали в src/domain/sync/service.ts; на текущих головах (fetch 16:16 NOVT) конфликтов нет ни в одной из 55 пар.
  • Codex, codex/fix-format-baseline: на 513cdff в service.ts/sync.test.ts было 232/130 строк сверх prettier, все есть в antigravity/fix-reject-device-shifts; на a05777c — только форматирование (19 файлов).
  • Codex, live-postgres-remediation.test.ts: выполняется только при DATABASE_TEST_ISOLATED=true, DATABASE_TEST_GUARD_TOKEN, таблице krona_staff_test_guard с этим маркером и имени БД krona_staff_audit_*. Соглашение нигде, кроме самого теста, не описано, и ни CI, ни scripts/test/* его не выполняют — в npm run check файл всегда skipped. accept-branch ставит предохранитель в одноразовой БД: 1 passed (1). Предложение: описать соглашение в README/AGENTS и после слияния включить его в CI (могу сделать в своей зоне, .forgejo/workflows/ci.yml).
  • N5 до сих пор в основе: tenant-isolation.test.ts красный на b0243d5 и всех ветках от f5ac179 (permission denied for table staff_employees); исправление — в grok/e2e-harness. Локальная сборка основа + fix-live-database + fix-sync-c0-bootstrap + grok/e2e-harness: интеграционные 7/7, храповик просит убрать ровно K3/N2, K4/N1, K13/SYNC-A, K13/SYNC-B, SYNC-C0.
  • Схема (низкий приоритет): staff_notification_deliveries.outbox_id → staff_outbox(id) — единственный внешний ключ без tenant_id; все остальные составные (tenant_id, …). База не мешает доставке ссылаться на событие outbox другого арендатора (RLS фильтрует чтение, но не ссылку). Предложение: UNIQUE (tenant_id, id) на staff_outbox и FK (tenant_id, outbox_id).
  • PILOT-REHOME: скрипта нет → [PILOT-REHOME] красный; контракт — C6.

8. Круг 3б (основа f7d3c73, 2026-09-26 ~17:00 NOVT)

Вывод — tests/e2e/report-evidence/round3-accept-f7d3c73.md, round3-k18-and-rehome.md.

  • codex/fix-tenant-config (535052e): npm run check красный — tests/unit/sync.test.ts:556:18 → STANDALONE_TENANT_ID uses the development placeholder… (строка 553 — заглушка, 563 — NODE_ENV: 'test'). Вывод владельца подтверждён; как исправить — ACCEPTANCE-3 (разрешить заглушку при NODE_ENV === 'test').
  • codex/fix-tenant-rehome (c73bb3b → d52ecf3): по существу перенос верный (копия после восстановления: счётчики, ledger, FK, аудит tenant.rehomed, изоляция, отказ повтора). [PILOT-REHOME] красный из-за интерфейса — таблица различий в C6. Интеграционный тест rehome-tenant.test.ts зелёный только с DATABASE_REHOME_TEST_URL/DATABASE_REHOME_TEST_CONFIRM — в npm run check и CI он всегда skipped. Тест требует подключения суперпользователя (rehome-tenant.test.ts:92-93) и содержит один it — запуск скрипта от не-суперпользователя (как krona_migrator в пилоте) не покрыт.
  • codex/fix-pilot-backups (a005c5e → b022885, K18): test-backup.sh/test-backup-pull.sh проходят (моки), но в npm run check и CI не входят. Учебное восстановление без Docker прошло (шим docker → pg_dump PG17, CMS, sha256, pg_restore, restore-drill-report.sql совпал). Замечания:
    • backup.sh снимает только pg_dump -Fc одной БД: ролей (globals) в копии нет. В кластере без ролей pg_restore → exit 1, 61 ошибка role "krona_migrator" does not exist. Шаг 4 runbook («Restore required roles and grants») нужно сделать исполнимым: скрипт создания ролей для восстановления или pg_dumpall --globals-only --no-role-passwords рядом с копией.
    • restore-drill-isolation.sql жёстко зашит на 01J00000000000000000000000 → 01M2R6AC5652NPT3MG34DKN4ZP; после переноса «source/target» поменяются местами — стоит параметризовать (psql -v).
    • В docs/BACKUP-RESTORE.md личный путь C:/Users/trush/Documents/KronaStaffBackups/ — в репозитории лучше указать роль или место без имени пользователя.
    • b022885: ротация копий на ПК есть (BACKUP_LOCAL_RETENTION_DAYS, 90 дней; в test-backup-pull.sh есть проверка), ротации на сервере нет. В новых scripts/run-pilot-backup-pull.ps1 и deploy/pilot/backup.home-server.example тоже зашиты личные пути. Контроль отказов таймера и задачи pull пока описан только в документе.
  • Предложение (моя зона, после слияния): добавить в CI bash scripts/test-backup.sh, bash scripts/test-backup-pull.sh и запуск rehome-tenant.test.ts / live-postgres-remediation.test.ts на одноразовых БД — так же, как это делает accept-branch.