Some checks are pending
ci / verify (push) Waiting to run
Executors could not see the audit: Grok reported that the owner's AUDIT.md and evidence never reached it, so its scenarios could not be mapped to the audit IDs. Keeping the audit, the acceptance report and the task for each executor in the repository lets Codex, Antigravity and Grok take work and report through git. The live-check scripts are stored as .mjs.txt so the project linter, which type-checks every .mjs file, does not break for everyone; the maintained version of these checks is Grok's harness in tests/e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
19 KiB
Markdown
116 lines
19 KiB
Markdown
# Задание для Antigravity, круг 2: синхронизация, push, outbox
|
||
|
||
- Репозиторий: https://git.ochenstarik.ru/ochenstarik/krona-staff
|
||
- Основа: текущая основная ветка `codex/bootstrap-platform-foundation` (`331e490`)
|
||
- Задание самодостаточное; полный аудит и доказательства — в `docs/audit/2026-09-26/`.
|
||
|
||
## Итог приёмки прошлого круга
|
||
|
||
Ветку `antigravity/fix-reject-device-shifts` (`795bba0`) проверили на живой PostgreSQL 17.
|
||
|
||
**Что сработало:**
|
||
|
||
- Подделка смены с устройства закрыта. Смена `status: confirmed, worked_minutes: 2880` теперь получает `rejected / COMMAND_NOT_SUPPORTED`. Раньше она начисляла 14 400 ₽, теперь 0 ₽.
|
||
- Сценарии Grok SYNC-A и SYNC-B на этой ветке зелёные.
|
||
|
||
**Что не так:**
|
||
|
||
1. `tests/integration/sync-rejection.test.ts:69-74` на реальной базе падает. Очистка идёт ролью приложения, а у неё нет права DELETE на `staff_timesheet_entries` и `staff_employees`. Проверка внутри теста проходит, но файл красный — значит, на живой базе тест не запускали.
|
||
2. Сообщение коммита — один абзац в строке заголовка. Нужен короткий заголовок, а причина — в теле.
|
||
3. Из задания сделана только немедленная мера. Остальные части не начаты.
|
||
|
||
## Как работать в этом круге
|
||
|
||
- **Ветки.** `antigravity/fix-<тема>` от `331e490`, по одной на каждый пункт ниже. Маленькие коммиты: заголовок до 72 символов, причина в теле (AGENTS.md).
|
||
- **Проверка на живой базе обязательна.** В репозитории есть стенд Grok (ветка `grok/e2e-harness`, после влития — в основной):
|
||
- `scripts/test/provision-pg.mjs` создаёт роли как в пилоте: мигратор без SUPERUSER, приложение без BYPASSRLS;
|
||
- `node scripts/test/run-e2e.mjs` прогоняет сценарии на реальном сервере и реальной PostgreSQL 17.
|
||
|
||
Пункт считается сделанным, когда соответствующий сценарий стал зелёным. Номера сценариев указаны в таблицах.
|
||
|
||
- **Очистка в тестах** — ролью мигратора, как в `tests/integration/tenant-isolation.test.ts` после исправления Grok. Роли приложения удалять запрещено, так задумано.
|
||
- **Список известных дефектов.** После каждого исправления сообщи Grok ID сценариев, которые стали зелёными, — он уберёт их из `tests/e2e/known-defects.json`. Сам этот файл не правь: иначе CI упадёт с «дефект исправлен, обновите список».
|
||
- **Outbox.** Писать в `staff_outbox` только через общий хелпер, который делает Codex (ветка `codex/fix-live-database`). Во вставке обязательно поле `occurred_at` — без него подтверждение и расчёт падают.
|
||
|
||
**Твоя зона:**
|
||
|
||
- `src/sync.ts`, `src/domain/sync/**`, миграции синхронизации;
|
||
- `src/domain/notification/**`, маршруты `/push/*`;
|
||
- service worker и подписка в `src/portal-ui.ts`;
|
||
- диспетчер и публикатор outbox.
|
||
|
||
**Не трогай:** авторизацию, сотрудников, смены, расчёт, терминал и развёртывание — это зона Codex; `tests/e2e/**` и CI — зона Grok.
|
||
|
||
## 1. Довести прошлую ветку (сегодня)
|
||
|
||
| Что сделать | Готово, когда |
|
||
| --------------------------------------------------------------- | ----------------------------------------- |
|
||
| Очистка в `sync-rejection.test.ts` — ролью мигратора | Тест зелёный на живой базе |
|
||
| Перебазировать ветку на `331e490`, переписать сообщение коммита | Ветка вливается в основную без конфликтов |
|
||
|
||
## 2. Синхронизация на реальной базе не работает (срочно)
|
||
|
||
| ID | Что сломано | Где | Готово, когда |
|
||
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------------- |
|
||
| SYNC-C0 | Начальная загрузка на устройство (`/sync/pull` без курсора) всегда отвечает 500: `column "status" does not exist`. Запрос обращается к столбцу, которого в таблице нет | `src/domain/sync/service.ts`, начальная загрузка | SYNC-C0 зелёный |
|
||
|
||
## 3. Синхронизация: остальные дефекты
|
||
|
||
| Мой ID | Сценарий Grok | Что сделать | Готово, когда |
|
||
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||
| S2 | SYNC-J | `occurred_at` — строгий ISO с часовым поясом. Устройство передаёт своё текущее время, сервер вычисляет расхождение часов и ограничивает время отметки относительно момента получения. При большом расхождении — `pending_review`. Сейчас отметка с датой 2020 года принимается | SYNC-J зелёный |
|
||
| S3 | SYNC-G | Сначала захватить ключ идемпотентности, потом выполнять команду. `SAVEPOINT` на каждую команду. Проверять ревизию в UPDATE. `source_event_id = sync:{device}:{key}`. Временные отказы не кэшировать | Битая команда не откатывает весь пакет; две параллельные отправки с одним ключом выполняются один раз |
|
||
| S4 | SYNC-E, SYNC-F | Начальная загрузка — только по точке устройства, только активные сотрудники, ограниченное окно дат, пагинация в SQL. Отметки уволенных отклонять. Точку брать из устройства; чужую точку в payload отклонять | SYNC-E и SYNC-F зелёные |
|
||
| S5 | SYNC-C, SYNC-D | Курсор не теряет данные при `has_more`. Порядок — по фиксации транзакций, а не по `BIGSERIAL`. Изменения из портала видны устройству. Устаревший курсор запускает повторную начальную загрузку | SYNC-C и SYNC-D зелёные |
|
||
| S6 | — | Отметки через синхронизацию пишут outbox (через общий хелпер), как онлайн-маршрут | Отметка через синхронизацию порождает событие |
|
||
| S7 | — | Очистка входящих получает `tenant_id`, а не идентификатор системного актора (`service.ts:1056`), и запускается по расписанию | — |
|
||
| S8 | SYNC-H | Проверка Origin на маршрутах управления устройствами, белый список scope, `no-store` на ответе с токеном устройства | SYNC-H зелёный |
|
||
| S9 | попроси Grok добавить | Внешний ключ `(tenant_id, org_unit_id) ON DELETE SET NULL` обнуляет и `tenant_id`, поэтому удаление точки с привязанным устройством падает с `23502`. Новой миграцией заменить на `SET NULL (org_unit_id)` или `RESTRICT` | Точку можно удалить или получить понятный отказ |
|
||
| — | SYNC-I | Без настроенных токенов анонимный запрос не должен регистрировать устройство. Сделать вместе с Codex: он закрывает открытый режим без токенов (F2) | SYNC-I зелёный |
|
||
| — | SYNC-K | Подтверждение курсора «из будущего» (больше выданного сервером) отклонять с 400 | SYNC-K зелёный |
|
||
|
||
## 4. Push-уведомления
|
||
|
||
| Мой ID | Сценарий Grok | Что сделать | Готово, когда |
|
||
| ------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
||
| K12 | N3 | SSRF: сейчас сотрудник подписывает push на `https://127.0.0.1:…`, и сервер сам туда подключается. Разрешать только известные push-сервисы (FCM, Mozilla, Apple, WNS). После DNS запрещать приватные, loopback и link-local адреса. `redirect: 'manual'`, таймаут 5–10 с, тело ответа не сохранять. Где: `push-dispatcher.ts:80`, `web-push.ts:226-258` | N3 зелёный |
|
||
| N9 | N9 | Конфликт endpoint с другим сотрудником — ответ 409. Перепривязка разрешена только тем же браузером | N9 зелёный |
|
||
| N8 | — | В production без `VAPID_*` сервер не стартует. Публичный ключ отдавать с `no-cache` | — |
|
||
| U1–U4 | — | Проверять ключи подписки при регистрации. Ошибка по одной подписке не прерывает отправку остальным. Не больше 5–10 подписок на сотрудника. Таймаут и лимит чтения ответа 1 КБ. Атомарный захват строки перед отправкой (`sending` плюс аренда), повторы с backoff, dead letter | Два экземпляра сервера не отправляют сообщение дважды |
|
||
| U5–U7 | — | Scope service worker покрывает `/staff`. Выход отзывает push-подписку устройства. Составной внешний ключ с `tenant_id` в `staff_notification_deliveries` | — |
|
||
|
||
## 5. Оповещения процесса подтверждения (после этапа 4 у Codex)
|
||
|
||
Решение владельца: бухгалтер отправляет пакет расчёта и выплат на подтверждение, руководитель получает оповещение, проверяет и подписывает, после чего всё регистрируется. Сам процесс и события делает Codex, от тебя — доставка.
|
||
|
||
| Событие | Кому | Содержание |
|
||
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
|
||
| `staff.payroll.package_submitted` | Всем с ролью «Руководитель» в арендаторе | Период, число сотрудников, итог, число предупреждений, ссылка на экран подписи |
|
||
| `staff.payroll.package_returned` | Бухгалтеру, который отправил пакет | Комментарий руководителя |
|
||
| `staff.payroll.package_signed` | Бухгалтеру и сотрудникам из пакета | Сотрудникам — только «начисление за период доступно», без сумм |
|
||
|
||
Основной канал — push. Запасной — Telegram, без персональных данных и сумм. Если пакет не подписан за 24 ч (значение настраивается), отправить повторное напоминание. Адресата определять по роли, а не по фиксированному списку.
|
||
|
||
Сценарий Grok OWN-03 («каждый руководитель получает ровно одно оповещение») должен стать зелёным.
|
||
|
||
## 6. Публикатор outbox
|
||
|
||
Вместо `setImmediate` нужен фоновый публикатор:
|
||
|
||
- повторяет строки в статусах `pending` и `failed`;
|
||
- ставит `published_at`;
|
||
- на стороне получателя — идемпотентность через inbox;
|
||
- соблюдает политику хранения.
|
||
|
||
Готово, когда после перезапуска сервера неотправленные события уходят, а `staff_outbox` не растёт бесконечно.
|
||
|
||
## Порядок
|
||
|
||
1. Пункт 1, затем пункт 2 — сегодня.
|
||
2. Пункты 3 и 4 — параллельно, по одной ветке на пункт.
|
||
3. Пункт 5 — когда Codex выпустит события пакета.
|
||
4. Пункт 6 — после влития общего хелпера outbox от Codex.
|
||
|
||
## Отчёт после каждой ветки
|
||
|
||
Ветка, коммит и ID сценариев Grok, которые стали зелёными, — с фактическим выводом `run-e2e`.
|