krona-staff/tests/e2e/report-evidence/round3-k18-and-rehome.md
grok 75f76430d4 docs(e2e): доказательства — повторная приёмка после d52ecf3 и b022885
Почему: пока шёл разбор, Codex запушил codex/fix-tenant-rehome d52ecf3
(17:02, синонимы C6) и codex/fix-pilot-backups b022885 (17:07, ротация на ПК).
Таблица по старым SHA устарела, поэтому все 7 веток прогнаны заново
(стенд a6a1b5e): round3-accept-f7d3c73-rerun.md.

В round3-k18-and-rehome.md, разд. 6: вывод [PILOT-REHOME] на d52ecf3 как есть
(многострочный JSON → «dry-run без JSON-отчёта»), пошаговая диагностика
в копии сценария, которую потом откатил (что упадёт дальше: сумма журнала
в dry-run, dry-run по умолчанию вместо переноса), и тесты backup на b022885.
2026-09-26 17:20:23 +07:00

26 KiB
Raw Blame History

K18 (codex/fix-pilot-backups a005c5e) и перенос арендатора (codex/fix-tenant-rehome c73bb3b) на восстановленной копии

PG17 17.11 локально, без Docker. Ветки исполнителей не менялись: отдельные detached worktree

/workspace/krona-staff-acceptance/diag-pilot-backups и diag-tenant-rehome.

1. Тесты Codex из ветки (моки docker/ssh/scp)

$ bash scripts/test-backup.sh; bash scripts/test-backup-pull.sh backup tests passed: syntax, CMS round-trip, pg_dump/OpenSSL/scp failures, no plaintext dump on disk (exit=0) backup pull tests passed: CMS+checksum, idempotence, stale/mismatch rejection, path validation, scp failure (exit=0)

2. Учебное восстановление: настоящий pg_dump → backup.sh → sha256 → расшифровка → pg_restore → отчёты

docker подменён шимом: 'docker compose exec -T postgres sh -eu -c ' выполняет локально

(pg_dump -U pgadmin -d krona_k18_src -Fc); источник — копия БД e2e (заглушка, посторонние арендаторы, данные e2e).

$ bash drill.sh == 1. источник: копия krona_acc_e2e (данные последнего e2e-прогона accept-branch) == 2. deploy/pilot/backup.sh (external-pull) backup=/server/backups/krona-staff-20260926T101055Z-1837466.dump.cms checksum=/server/backups/krona-staff-20260926T101055Z-1837466.dump.cms.sha256 off_host_copy=pending_pull exit=0 plaintext на сервере: 0 файлов первые байты (DER): 30 80 06 09 == 3. «перенос на ПК» (cp вместо ssh/scp — pull-pilot-backups.sh здесь не запускался) + sha256 krona-staff-20260926T101055Z-1837466.dump.cms: OK == 4. расшифровка ключом вне сервера + pg_restore в новую БД TABLE DATA в дампе: 28 pg_restore exit=0 == 5. restore-drill-report.sql: источник vs копия строк отчёта: 79; diff: идентичны RLS|27|t LEDGER||RUB|263568 LEDGER||RUB|22200 LEDGER||RUB|329705 schema_migrations: 11 staff_payroll_ledger owner=krona_migrator гранты krona_staff_app: 100 == 6. restore-drill-isolation.sql (krona_staff_app, NOBYPASSRLS) BEGIN SET SET source-employee-count|3 SET target-employee-count|0 ROLLBACK

3. Перенос заглушки на копии, интерфейс продукта как есть

$ bash rehome-on-copy.sh # DATABASE_REHOME_URL=<суперпользователь>@krona_k18_restore == dry-run: node scripts/rehome-tenant.mjs --dry-run --source --target exit=0 {"mode":"dry-run","source":"","target":"","tables":{"staff_attendance_events":"2","staff_audit_log":"2","staff_daily_metrics":"2","staff_employees":"3","staff_idempotency_keys":"2","staff_inbox":"2","staff_metric_definitions":"2","staff_notification_deliveries":"2","staff_org_unit_terms":"2","staff_org_units":"2","staff_outbox":"2","staff_pay_assignments":"2","staff_pay_rules":"2","staff_payouts":"2","staff_payroll_ledger":"3","staff_payroll_lines":"2","staff_payroll_runs":"2","staff_professions":"2","staff_push_subscriptions":"2","staff_standalone_employee_access":"2","staff_sync_change_log":"2","staff_sync_command_inbox":"2","staff_sync_device_checkpoints":"2","staff_sync_devices":"2","staff_timesheet_entries":"2","staff_work_items":"2","staff_work_sessions":"2"}}

dry-run: БД не изменилась == C6-интерфейс на этом же скрипте Tenant rehome stopped. Unknown option --from. No connection details or row contents were printed. exit=1 == apply без --backup-evidence Tenant rehome stopped. --backup-evidence is required for --apply. No connection details or row contents were printed. exit=1 == apply: --apply --source --target --actor --backup-evidence evidence.json exit=0 {"mode":"apply","source":"","target":"","tables":{"staff_attendance_events":"2","staff_audit_log":"3","staff_daily_metrics":"2","staff_employees":"3","staff_idempotency_keys":"2","staff_inbox":"2","staff_metric_definitions":"2","staff_notification_deliveries":"2","staff_org_unit_terms":"2","staff_org_units":"2","staff_outbox":"2","staff_pay_assignments":"2","staff_pay_rules":"2","staff_payouts":"2","staff_payroll_ledger":"3","staff_payroll_lines":"2","staff_payroll_runs":"2","staff_professions":"2","staff_push_subscriptions":"2","staff_standalone_employee_access":"2","staff_sync_change_log":"2","staff_sync_command_inbox":"2","staff_sync_device_checkpoints":"2","staff_sync_devices":"2","staff_timesheet_entries":"2","staff_work_items":"2","staff_work_sessions":"2"},"auditRowsAdded":1,"payrollLedger":[{"currency":"RUB","amountMinor":"263568"}]}

== сверка (restore-drill-report.sql до/после; счётчики по таблице/арендатору, RLS, ledger) -- строки с после: 0 -- прочие арендаторы до/после идентичны: да -- счётчики до vs после (кроме audit): совпадают -- audit: до 2, после 3 LEDGER||RUB|263568 LEDGER||RUB|22200 LEDGER||RUB|329705 до: LEDGER||RUB|263568 до: LEDGER||RUB|22200 до: LEDGER||RUB|329705 tenant.rehomed tenant_rehome FK после: 30 == krona_staff_app source-employee-count|0 target-employee-count|3 ERROR: permission denied for table staff_payroll_ledger == повторный apply Tenant rehome stopped. Target tenant already contains staff data; refusing merge. No connection details or row contents were printed. exit=1 повтор: БД не изменилась FK (определения + convalidated), md5 по всем FK: источник 54ee68489eced699e40a343f4ba4134e, копия после переноса 54ee68489eced699e40a343f4ba4134e

4. Сценарий [PILOT-REHOME] через диагностический адаптер C6 → c73bb3b (адаптер НЕ в продукте и НЕ в сценарии)

Вариант 1: адаптер только переводит флаги/переменные/формат вывода.

$ E2E_DATABASE_NAME=krona_diag_rehome_e2e node scripts/test/run-e2e.mjs ✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 183ms × tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 96ms → expected 'undefined' to be '328228' // Object.is equality

Вариант 2: адаптер дополнительно сам досчитывает SUM(amount_minor) для --dry-run (продукт её там не печатает).

✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME-SEED] сиды покрывают все таблицы с tenant_id (список из information_schema) 177ms ✓ tests/e2e/60-pilot-rehome.e2e.ts > PILOT-REHOME: перенос заглушки арендатора на настоящий tenant > [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 460ms

5. Восстановление в «голый» кластер (ролей нет)

Тот же pg_dump -Fc (источник krona_k18_src, владелец таблиц krona_migrator, как в стенде e2e), восстановление во временный кластер PG17 (initdb, порт 55439), где ролей krona_* нет:

$ createdb krona_staff (свежий кластер PG17, ролей krona_* нет) && pg_restore --exit-on-error -d krona_staff d.dump
pg_restore: error: could not execute query: ERROR:  role "krona_migrator" does not exist
Command was: ALTER SCHEMA public OWNER TO krona_migrator;
exit=1
$ pg_restore без --exit-on-error (как по умолчанию)
exit=1
ошибок: 61
pg_restore: error: could not execute query: ERROR:  role "krona_migrator" does not exist
154          # staff_employees: строки восстановлены
postgres     # владелец staff_employees
0            # ролей krona_* в кластере

В пилоте владелец — POSTGRES_USER, а krona_staff_app создаёт docker/postgres/init-app-role.sh при инициализации контейнера — там роли, вероятно, будут; без Docker не проверено.

Исходный код адаптера (вариант 2)

// ДИАГНОСТИЧЕСКИЙ АДАПТЕР (не коммитится в продукт, не часть сценария): переводит контракт C6
// (--from/--to/--dry-run/--json, DATABASE_ADMIN_URL, одна строка JSON с ledger_amount_minor_sum)
// в интерфейс codex/fix-tenant-rehome c73bb3b (--source/--target, --dry-run|--apply, --actor,
// --backup-evidence, DATABASE_REHOME_URL, многострочный JSON). Evidence — синтетическая.
import { spawnSync } from 'node:child_process';
import { writeFileSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const a = process.argv.slice(2);
const get = (k) => a[a.indexOf(k) + 1];
const dry = a.includes('--dry-run');
const args = ['--source', get('--from'), '--target', get('--to')];
if (dry) args.unshift('--dry-run');
else {
  const dir = mkdtempSync(path.join(tmpdir(), 'rehome-ev-'));
  const sha = 'b'.repeat(64);
  writeFileSync(
    path.join(dir, 'e.json'),
    JSON.stringify({
      backup: {
        format: 'pg_dump -Fc',
        encrypted: true,
        offHost: true,
        sha256: sha,
        createdAt: '2026-09-26T10:00:00Z',
      },
      restoreDrill: {
        status: 'passed',
        backupSha256: sha,
        restoredAt: '2026-09-26T10:01:00Z',
        verifiedAt: '2026-09-26T10:02:00Z',
        isolatedDatabase: 'diag',
      },
    }),
  );
  args.unshift('--apply');
  args.push('--actor', '01M3ACT0R000000000000000AA', '--backup-evidence', path.join(dir, 'e.json'));
}
const r = spawnSync(
  process.execPath,
  [path.join(import.meta.dirname, 'rehome-tenant.product.mjs'), ...args],
  {
    env: { ...process.env, DATABASE_REHOME_URL: process.env.DATABASE_ADMIN_URL },
    encoding: 'utf8',
  },
);
process.stderr.write(r.stderr);
if (r.status !== 0) process.exit(r.status ?? 1);
const rep = JSON.parse(r.stdout);
const out = {
  tables: Object.fromEntries(Object.entries(rep.tables).map(([k, v]) => [k, Number(v)])),
};
if (rep.payrollLedger)
  out.ledger_amount_minor_sum = rep.payrollLedger
    .reduce((s, x) => s + BigInt(x.amountMinor), 0n)
    .toString();
if (dry && !out.ledger_amount_minor_sum) {
  // ВАРИАНТ 2: сумму досчитывает сам адаптер (продукт её в dry-run не печатает), чтобы проверить остальные шаги.
  const pg = (await import('pg')).default;
  const c = new pg.Client({ connectionString: process.env.DATABASE_ADMIN_URL });
  await c.connect();
  out.ledger_amount_minor_sum = (
    await c.query(
      'SELECT coalesce(sum(amount_minor),0)::text s FROM staff_payroll_ledger WHERE tenant_id=$1',
      [get('--from')],
    )
  ).rows[0].s;
  await c.end();
}
console.log(JSON.stringify(out));

Исходный код drill.sh

#!/usr/bin/env bash
# K18: backup.sh (codex/fix-pilot-backups a005c5e) → шифрованный файл → проверка sha256 → расшифровка
# → pg_restore в новую БД → restore-drill-report.sql на источнике и копии → сравнение → isolation.
# Docker нет: `docker compose exec -T postgres sh -eu -c '<cmd>'` подменён шимом, который выполняет
# ту же команду pg_dump локально против PG17 (суперпользователь, как POSTGRES_USER в образе postgres).
set -euo pipefail
WT=/workspace/krona-staff-acceptance/diag-pilot-backups
PGBIN=/workspace/.mamba-root/envs/pg17/bin
export PGHOST=127.0.0.1 PGPORT=55433 PGUSER=pgadmin PGPASSWORD="$(cat /workspace/.pg17-secrets/admin.pw)"
psql() { "$PGBIN/psql" -X -v ON_ERROR_STOP=1 "$@"; }
T=$(mktemp -d /tmp/k18-drill.XXXXXX); trap 'rm -rf "$T"' EXIT
mkdir -p "$T/bin" "$T/server" "$T/pc" "$T/offhost-key"
echo "== 1. источник: копия krona_acc_e2e (данные последнего e2e-прогона accept-branch)"
psql -d postgres -q -c "DROP DATABASE IF EXISTS krona_k18_src" -c "DROP DATABASE IF EXISTS krona_k18_restore" \
  -c "CREATE DATABASE krona_k18_src TEMPLATE krona_acc_e2e OWNER krona_migrator" \
  -c "REVOKE ALL ON DATABASE krona_k18_src FROM PUBLIC" -c "GRANT CONNECT ON DATABASE krona_k18_src TO krona_staff_app"
cat > "$T/bin/docker" <<SHIM
#!/bin/sh
for last; do :; done
exec env PATH="$PGBIN:\$PATH" POSTGRES_USER=pgadmin POSTGRES_DB=krona_k18_src sh -eu -c "\$last"
SHIM
chmod +x "$T/bin/docker"
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 1 -keyout "$T/offhost-key/recipient.key" \
  -out "$T/server/recipient.crt" -subj '/CN=k18-drill' >/dev/null 2>&1
echo "== 2. deploy/pilot/backup.sh (external-pull)"
PATH="$T/bin:$PATH" BACKUP_DIRECTORY="$T/server/backups" BACKUP_RECIPIENT_CERT="$T/server/recipient.crt" \
  BACKUP_TRANSFER_MODE=external-pull sh "$WT/deploy/pilot/backup.sh" | sed "s#$T#<tmp>#g"
echo "exit=$?"
f=$(ls "$T/server/backups"/*.dump.cms); n=$(basename "$f")
echo "plaintext на сервере: $(find "$T/server/backups" -type f ! -name '*.dump.cms' ! -name '*.dump.cms.sha256' | wc -l) файлов"
head -c 4 "$f" | od -An -tx1 | sed 's/^/первые байты (DER): /'
echo "== 3. «перенос на ПК» (cp вместо ssh/scp — pull-pilot-backups.sh здесь не запускался) + sha256"
cp "$f" "$f.sha256" "$T/pc/" && (cd "$T/pc" && sha256sum -c "$n.sha256")
sha=$(cut -d' ' -f1 "$T/pc/$n.sha256")
echo "== 4. расшифровка ключом вне сервера + pg_restore в новую БД"
openssl cms -decrypt -binary -inform DER -in "$T/pc/$n" -recip "$T/server/recipient.crt" \
  -inkey "$T/offhost-key/recipient.key" -out "$T/pc/restore.dump"
"$PGBIN/pg_restore" -l "$T/pc/restore.dump" | grep -c "TABLE DATA" | sed 's/^/TABLE DATA в дампе: /'
psql -d postgres -q -c "CREATE DATABASE krona_k18_restore TEMPLATE template0 OWNER krona_migrator" \
  -c "REVOKE ALL ON DATABASE krona_k18_restore FROM PUBLIC" -c "GRANT CONNECT ON DATABASE krona_k18_restore TO krona_staff_app"
"$PGBIN/pg_restore" --exit-on-error -d krona_k18_restore "$T/pc/restore.dump"; echo "pg_restore exit=$?"
echo "== 5. restore-drill-report.sql: источник vs копия"
psql -d krona_k18_src -At -f "$WT/scripts/restore-drill-report.sql" > "$T/src.txt"
psql -d krona_k18_restore -At -f "$WT/scripts/restore-drill-report.sql" > "$T/restore.txt"
echo "строк отчёта: $(wc -l < "$T/src.txt"); diff:"; diff "$T/src.txt" "$T/restore.txt" && echo "идентичны"
grep -E '^(RLS|LEDGER)' "$T/restore.txt" | sed -E 's/[0-9A-HJKMNP-TV-Z]{26}/<ulid>/g'
psql -d krona_k18_restore -At -c "select count(*) from schema_migrations" | sed 's/^/schema_migrations: /'
psql -d krona_k18_restore -At -c "select tablename||' owner='||tableowner from pg_tables where schemaname='public' and tablename='staff_payroll_ledger'"
psql -d krona_k18_restore -At -c "select count(*) from information_schema.role_table_grants where grantee='krona_staff_app'" | sed 's/^/гранты krona_staff_app: /'
echo "== 6. restore-drill-isolation.sql (krona_staff_app, NOBYPASSRLS)"
psql -d krona_k18_restore -At -f "$WT/scripts/restore-drill-isolation.sql"
printf '%s\n' "$sha" > /tmp/ab/k18/backup.sha256

Исходный код rehome-on-copy.sh

#!/usr/bin/env bash
# TASK-CODEX-2 п.2.2 на восстановленной копии (krona_k18_restore из drill.sh), интерфейс продукта c73bb3b как есть.
set -uo pipefail
WT=/workspace/krona-staff-acceptance/diag-tenant-rehome
BK=/workspace/krona-staff-acceptance/diag-pilot-backups
PGBIN=/workspace/.mamba-root/envs/pg17/bin
export PGHOST=127.0.0.1 PGPORT=55433 PGUSER=pgadmin PGPASSWORD="$(cat /workspace/.pg17-secrets/admin.pw)"
DB=krona_k18_restore
URL="postgres://pgadmin:${PGPASSWORD}@127.0.0.1:55433/$DB"
STUB=01J00000000000000000000000; TARGET=01M2R6AC5652NPT3MG34DKN4ZP
psql() { "$PGBIN/psql" -X -v ON_ERROR_STOP=1 "$@"; }
mask() { sed -E "s/$STUB/<STUB>/g; s/$TARGET/<TARGET>/g; s/[0-9A-HJKMNP-TV-Z]{26}/<other>/g"; }
cd "$WT"
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/before.txt
echo "== dry-run: node scripts/rehome-tenant.mjs --dry-run --source <STUB> --target <TARGET>"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --dry-run --source $STUB --target $TARGET > /tmp/ab/k18/dry.json; echo "exit=$?"
mask < /tmp/ab/k18/dry.json | tr -d '\n ' | cut -c1-900; echo
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/after-dry.txt
diff -q /tmp/ab/k18/before.txt /tmp/ab/k18/after-dry.txt && echo "dry-run: БД не изменилась"
echo "== C6-интерфейс на этом же скрипте"
DATABASE_ADMIN_URL="$URL" node scripts/rehome-tenant.mjs --from $STUB --to $TARGET --dry-run --json; echo "exit=$?"
echo "== apply без --backup-evidence"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA; echo "exit=$?"
sha=$(cat /tmp/ab/k18/backup.sha256)
cat > /tmp/ab/k18/evidence.json <<JSON
{"backup":{"format":"pg_dump -Fc","encrypted":true,"offHost":true,"sha256":"$sha","createdAt":"2026-09-26T10:10:55Z"},
 "restoreDrill":{"status":"passed","backupSha256":"$sha","restoredAt":"2026-09-26T10:11:00Z","verifiedAt":"2026-09-26T10:11:05Z","isolatedDatabase":"$DB"}}
JSON
echo "== apply: --apply --source <STUB> --target <TARGET> --actor <ulid> --backup-evidence evidence.json"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA --backup-evidence /tmp/ab/k18/evidence.json > /tmp/ab/k18/apply.json; echo "exit=$?"
mask < /tmp/ab/k18/apply.json | tr -d '\n ' | cut -c1-1200; echo
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" > /tmp/ab/k18/after.txt
echo "== сверка (restore-drill-report.sql до/после; счётчики по таблице/арендатору, RLS, ledger)"
echo "-- строки с <STUB> после: $(grep -c "|$STUB|" /tmp/ab/k18/after.txt)"
echo "-- прочие арендаторы до/после идентичны: $(diff <(grep -v -e "|$STUB|" -e "|$TARGET|" /tmp/ab/k18/before.txt) <(grep -v -e "|$STUB|" -e "|$TARGET|" /tmp/ab/k18/after.txt) >/dev/null && echo да || echo НЕТ)"
echo "-- счётчики <STUB> до vs <TARGET> после (кроме audit):"
diff <(grep "|$STUB|" /tmp/ab/k18/before.txt | grep -v '^staff_audit_log' | sed "s/$STUB/X/") <(grep "|$TARGET|" /tmp/ab/k18/after.txt | grep -v '^staff_audit_log' | sed "s/$TARGET/X/") && echo "   совпадают"
echo "-- audit: до $(grep "^staff_audit_log|$STUB|" /tmp/ab/k18/before.txt | cut -d'|' -f3), после $(grep "^staff_audit_log|$TARGET|" /tmp/ab/k18/after.txt | cut -d'|' -f3)"
grep "^LEDGER" /tmp/ab/k18/after.txt | mask; grep "^LEDGER" /tmp/ab/k18/before.txt | mask | sed 's/^/до: /'
psql -d $DB -At -c "select action||' '||entity_type from staff_audit_log where tenant_id='$TARGET' and action like 'tenant.%'"
psql -d $DB -At -c "select count(*) from pg_constraint where contype='f' and connamespace='public'::regnamespace" | sed 's/^/FK после: /'
echo "== krona_staff_app"
psql -d $DB -At -f "$BK/scripts/restore-drill-isolation.sql" | grep count
psql -d $DB -At -c "BEGIN; SET LOCAL ROLE krona_staff_app; SET LOCAL app.tenant_id='$TARGET'; UPDATE staff_payroll_ledger SET amount_minor=amount_minor+1;" 2>&1 | tail -1
echo "== повторный apply"
DATABASE_REHOME_URL="$URL" node scripts/rehome-tenant.mjs --apply --source $STUB --target $TARGET --actor 01M3ACT0R000000000000000AA --backup-evidence /tmp/ab/k18/evidence.json; echo "exit=$?"
psql -d $DB -At -f "$BK/scripts/restore-drill-report.sql" | diff -q - /tmp/ab/k18/after.txt && echo "повтор: БД не изменилась"

6. Ветки сдвинулись во время приёмки (17:02 и 17:07 NOVT)

После git fetch (~17:13 NOVT): codex/fix-tenant-rehome c73bb3b → d52ecf3 (fix: accept audit harness tenant rehome CLI aliases, 17:02:16 +0700), codex/fix-pilot-backups a005c5e → b022885 (ops: schedule home pilot backup and retain verified PC copies, 17:07:36 +0700). Приёмка повторена по всем 7 веткам: round3-accept-f7d3c73-rerun.md (стенд a6a1b5e, логи tests/e2e/evidence/accept/2026-09-26T10-14-42/).

6.1 d52ecf3: что изменилось в интерфейсе

git diff c73bb3b origin/codex/fix-tenant-rehome -- scripts/rehome-tenant.mjs: добавлены --from/--to (синонимы --source/--target), --json (принимается, вывод и так JSON), DATABASE_ADMIN_URL (первым, дальше DATABASE_REHOME_URL, DATABASE_MIGRATION_URL); result.mode ??= 'dry-run' — без флага теперь dry-run, а не ошибка; --apply по-прежнему требует --actor и --backup-evidence. Вывод остался многострочным (JSON.stringify(…, null, 2)), счётчики — строки, в dry-run суммы журнала нет.

6.2 [PILOT-REHOME] на d52ecf3, как есть (accept-branch)

× … [PILOT-REHOME] --dry-run, перенос админом, сверка, повторный запуск отказывает 73ms
AssertionError: dry-run без JSON-отчёта: {
  "mode": "dry-run",
  "source": "01J00000000000000000000000",
  "target": "01M2R6AC5652NPT3MG34DKN4ZP",
  "tables": {
    "staff_attendance_events": "2",
    …
  }
}
: expected null not to be null
 ❯ tests/e2e/60-pilot-rehome.e2e.ts:468:70

Интерфейс C6: «--json: последней строкой stdout — {…}»; продукт печатает JSON на нескольких строках.

6.3 Диагностика: что упадёт следующим (правки только в неотслеживаемой копии сценария в worktree приёмки, потом откатил)

node scripts/test/run-e2e.mjs в wt-codex-fix-tenant-rehome (d52ecf3), каждый шаг добавляет одну правку к предыдущему:

Правка в копии сценария Итог [PILOT-REHOME]
парсер принимает весь stdout как JSON expected { staff_attendance_events: '2', …(26) } to deeply equal { staff_attendance_events: 2, …(26) }
+ счётчики через Number() (это ошибка сценария, исправлена в d477dde) expected 'undefined' to be '375888' — в dry-run нет ledger_amount_minor_sum
+ убрана проверка суммы в dry-run под заглушкой остались строки: expected [ 'staff_attendance_events', …(26) ] to deeply equal [] — вызов без флага (--json) теперь dry-run, exit 0, переноса нет

После отката: diff -q tests/e2e/60-pilot-rehome.e2e.ts /workspace/krona-staff/tests/e2e/60-pilot-rehome.e2e.ts && echo restored → restored.

6.4 b022885 (K18)

$ git worktree add --detach …/diag-pb2 origin/codex/fix-pilot-backups   # b022885
$ bash scripts/test-backup.sh | tail -1
backup tests passed: syntax, CMS round-trip, pg_dump/OpenSSL/scp failures, no plaintext dump on disk
test-backup rc=0
$ bash scripts/test-backup-pull.sh | tail -1
backup pull tests passed: CMS+checksum, idempotence, stale/mismatch rejection, path validation, scp failure
test-backup-pull rc=0

Новое: ротация копий на ПК в pull-pilot-backups.sh (BACKUP_LOCAL_RETENTION_DAYS, по умолчанию 90; удаляет только пары с корректным sha256, симлинки и посторонние файлы оставляет; в test-backup-pull.sh есть сценарий local-retention), deploy/pilot/krona-staff-backup-user.service (пользовательский systemd-сервис), scripts/run-pilot-backup-pull.ps1 (задача Windows), deploy/pilot/backup.home-server.example. Ротации на сервере нет (в docs/BACKUP-RESTORE.md: «server-side retention policy» — условие включения расписания). В run-pilot-backup-pull.ps1 и backup.home-server.example зашиты личные пути (C:\Users\trush\…, /home/ochenstarik/…). Systemd-юнит, задача Windows и ротация на реальном диске — не проверено.