Журнал строительных сессий
Append-only. Новые записи добавляются вниз. Чужие записи не правятся и не удаляются. Протокол —
START-HERE.md. Статусы единиц —ROADMAP.md.
Читать при старте сессии: последние 2–3 записи (tail -n 120) плюс запись той единицы, от
которой ты зависишь, — там сказано, что осталось недоделанным и где лежат заглушки.
Протокол и нарезка сменились после
F1. Роадмап перекроен с 35 технических единиц на 27 срезов продукта; начиная сF2сессия идёт по скиллуcrewsforge-session: бизнес-бриф с гейтом до захвата, без отдельной спеки, без остановки на плане, одно итоговое ревью и обязательный сквозной HTTP-e2e. ЗаписьF1сделана по прежнему протоколу — это нормально, переписывать её не нужно. Соответствие старых и новых ID — в записи «Сессия 0-бис».
Шаблоны
Заголовок раздела «Шаблоны»Открывающая запись (пишется на шаге 3 протокола, до создания ветки):
## <ID> — <название единицы>
**Открыта:** YYYY-MM-DD · **Статус записи:** в работе**Зависимости на момент захвата:** <ID>: DONE, <ID>: DONE**Ветка:** feat/<id>-<slug>
### Бизнес-бриф (утверждён Stark'ом)**Задача:** <что станет возможно в продукте>**Сущности:** <списком>**API для фронта:** <таблица: метод, путь, кто вызывает, назначение>**Можно собрать на фронте:** <экраны или куски сценария>**Чего ещё нельзя:** <честная граница>**Правки скоупа при утверждении:** <нет / что изменил Stark>Закрывающая запись (дописывается в тот же блок на шаге 11):
**Закрыта:** YYYY-MM-DD · **Статус записи:** закрыта · **PR:** [#31](https://github.com/crewsforge/crewsforge-back/pull/31)
### Сделано- <что реально появилось в коде: либы, модули, машины, эндпоинты, миграции>
### Решения реализации- <решение>: <выбранный вариант> — <обоснование в одну строку>- <оппонент: CONFIRMED / что исправлено после возражений>
### Приёмка- <критерий из карточки> — <чем доказан: имя теста / команда и её вывод>- HTTP-e2e: <имя теста> — сценарий и результат- Swagger: <путь> · маршруты из брифа на месте: да / расхождения- Ревью: <тир> — <вердикты> · отклонённые замечания и почему
### Отклонения от проектных документов- <нет> либо: <какое расхождение, какой step-документ поправлен, кем подтверждено>
### Заглушки и долги- <нет> либо: <что заглушено, где TODO, какая единица снимает>
### Хендофф следующей сессии- <что важно знать тому, кто возьмёт зависимую единицу: контракты, имена, подводные камни>Если сессия закрылась без завершения единицы (блокер), вместо закрывающей записи:
**Прервана:** YYYY-MM-DD · **Статус записи:** блокер · **Статус единицы:** BLOCKED
### Что успела- <...>
### Блокер- <в чём упёрлась; ссылка на пункт OPEN-QUESTIONS.md>
### Состояние ветки- <что закоммичено, что не закончено, безопасно ли продолжать поверх>Сессия 0 — подготовка контура
Заголовок раздела «Сессия 0 — подготовка контура»Открыта: 2026-08-21 · Закрыта: 2026-08-21 · Статус записи: закрыта · PR: —
Сделано
Заголовок раздела «Сделано»- Создан контур управления стройкой:
docs/delivery/—START-HERE.md,ROADMAP.md,SESSION-LOG.md,OPEN-QUESTIONS.md. - 20 эпиков шага 9 разложены на 35 единиц работы; все L-эпики (
F3,A1,A2,B2,B3,B5,C4) разбиты на подъединицы с явными границами и критериями приёмки. - Зафиксирован протокол сессии: захват → ветка →
brainstorming→writing-plans→subagent-driven-development→ ревью → документация → PR.
Отклонения от проектных документов
Заголовок раздела «Отклонения от проектных документов»- Порядок подъединиц
A1изменён на хронологический:A1.2— Offer и мэтчинг,A1.3— контрактинг (контрактинг каскадно порождается принятым оффером). Содержание эпикаA1из шага 9 не изменилось. - Уточнена граница
F2/C1: модель и транзакционная запись outbox — вF2, запуск relay-процесса — вC1. Шаг 9 упоминал relay в обоих эпиках. - Добавлена зависимость
F3.2 → F5: резолвер актора требует claimplatformRole. В шаге 9 эпикF3зависел только отF1,F2.
Хендофф следующей сессии
Заголовок раздела «Хендофф следующей сессии»- Первые готовые к захвату единицы:
F1иF4— обе без зависимостей, обе на критическом пути. Разумный порядок —F1, затемF4, затемF2иF5(параллелизуются), затемF3.1. - Репозиторий на момент записи не содержит ни
billing-api, ниplatform-worker, ни team-либ, ни либыstate-machine. Всё это создаётся с нуля в волне 1–2. - Ветка на момент записи —
feat/docs-rules.
F1 — Shared-либы ядра
Заголовок раздела «F1 — Shared-либы ядра»Открыта: 2026-08-21 · Статус записи: в работе Зависимости на момент захвата: — (единица без зависимостей) Ветка: feat/f1-shared-core-libs
Закрыта: 2026-08-21 · Статус записи: закрыта · PR: #29
Сделано
Заголовок раздела «Сделано»- Новая группа Nx-проектов
libs/apis/core/— три либы без единой зависимости, в том числе от NestJS: ни модулей, ни провайдеров, ни DI. api-core-money—calculateCommissionFee,splitSettlementнаbigintминорных единицах; порядок шага 5 §4: доля команды вниз, разница фаундеру, комиссияceilот доли команды.api-core-domain-events— 12 routing keys, четыре payload’а, карта «ключ → payload», union’ыActorType/SubjectType/TimerKind/WebhookProvider, структурный запрет денег в payload.api-core-platform-calendar—isWorkingTime,addWorkingHours,addWorkingDays,addCalendarDaysна голомDateв UTC, календарь параметром с дефолтом-константой.
Приёмка
Заголовок раздела «Приёмка»- Сумма компонент сплита всегда равна холду —
splitSettlement invariants, 539 комбинаций сумм, ставок и долей (включаяshareBps = 3333, нечётные остатки,2^100 + 3). - Комиссия по
ceil, неround—rounds the commission up, never down: подменаceilнаfloorвалит 230 случаев прогона (проверено мутацией). - Пятница 17:00 UTC + 4 рабочих часа → понедельник 13:00 —
carries the remainder over the weekend. - 5 рабочих дней не проскакивают выходные —
steps over the weekend instead of counting it. - Денежных полей в payload нет структурно — 13 типовых утверждений плюс гейт
CatalogIsMoneyFree: денежный payload в каталоге даётTest Suites: 1 failed(проверено поломкой). npx nx affected -t lint,test,build --base=main --skip-nx-cache— зелёный, 1634 + 31 + 2 теста.
Отклонения от проектных документов
Заголовок раздела «Отклонения от проектных документов»- Шаг 2 §9,
TimerKind— добавленыENVELOPE_EXPIRYиMILESTONE_DEADLINE: шаг 7 §3 описывает семь таймеров, шаг 2 объявлял пять. Подтверждено Stark’ом, врезка о ревизии внесена в step-документ, дельта — в карточкуF4. - Шаг 2 §9,
SubjectType— добавленоCONCIERGE_THREAD: консьерж-тред введён шагом 7 §5 и публикуетconcierge.escalated, аOutboxEvent.aggregateTypeтипизированSubjectType. Подтверждено Stark’ом, дельта — в карточкуF4. - Шаг 2 §7, проза об округлении — «метод наибольших остатков» заменён порядком шага 5 §4.
Вопрос был уже решён карточкой
F1, поэтому правка внесена без отдельного согласования. - Payload шины дополнен
subjectType(им типизированOutboxEvent.aggregateType) и необязательнымactorRole(шаг 2 §9 хранит роль отдельно отActorType). КарточкаF1дописана. - Схема имён Nx-проектов расширена до
api-core-<name>; строка добавлена в таблицуcoding-rules.md§1.2.
Заглушки и долги
Заголовок раздела «Заглушки и долги»- Заглушек нет. Отложено осознанно, с указанием единицы: конвертация
bigint → numberдля Stripe —B2.1; денежный линтер иdepConstraintsна границуapi-core-money—C4.1; подграфcoreв mermaid-диаграммеarchitecture.md—F2, вместе с первым ребром. docs/DOC.zipпо ошибке уехал в историю ветки коммитом327c31f(git add -A docs/). Файл возвращён в untracked,*.zipдобавлен в.gitignore, но блоб остаётся достижимым из того коммита: вычистить его можно только переписыванием ветки, и это решение Stark’а.
Хендофф следующей сессии
Заголовок раздела «Хендофф следующей сессии»F4— Prisma-enum’ыActorType(3),SubjectType(11),TimerKind(7),WebhookProvider(3) обязаны совпасть с union’ами@crewsforge-back/apis/core/domain-eventsзначение в значение. Требование записано в карточкуF4.B3.1—founderRefundMinorизSettlementSplitложится в колонкуfounder_amount_minorсхемы: имена намеренно разные, маппинг тривиален.A3/B3.1—splitSettlementпринимает одинshareBps. Допущение:teamShareBpsиcompletedCriteriaBps— два источника одной доли, а не сомножители. Если окажется иначе, правится вызывающий, не либа.- Проверок
buildиtypecheckу либ нет — Nx выводит толькоlintиtest, а eslint не типизирован. Из-за этого компиляционные гейтыdomain-eventsподключены к спеке side-effect-импортом. Единице, которая заведёт настоящийtypecheck-таргет, стоит проверить, не стал ли этот приём лишним.
Сессия 0-бис — перекрой роадмапа на срезы продукта
Заголовок раздела «Сессия 0-бис — перекрой роадмапа на срезы продукта»Открыта: 2026-08-30 · Закрыта: 2026-08-30 · Статус записи: закрыта · PR: — Ветка: feat/delivery-vertical-recut
Сделано
Заголовок раздела «Сделано»- Роадмап перекроен с 35 технических единиц на 27 срезов продукта. Причина: единица,
нарезанная по слою (либа, машина, схема), закрывается формально зелёными тестами, не давая
ничего вызываемого из фронта. Каждый срез, кроме
F1,F2иG1–G3, обязан закончиться работающими эндпоинтами. - Инфраструктура перестала быть отдельными единицами:
state-machineстроится вT2вместе с первой настоящей машиной (статус команды), очередь задач вO1— вместе с первой задачей, уведомления вO2— вместе с первым уведомлением. - Каждая карточка отвечает на пять вопросов до захвата: бизнес-задача · сущности · что появится для фронта · чего ещё нельзя · чем доказывается.
- Обязательный сквозной HTTP-e2e по реальной БД введён в критерии приёмки всех единиц с фронтовой поверхностью. Мокать транзакции, CAS-переходы, констрейнты БД и guard’ы авторизации запрещено; внешних провайдеров — можно.
- Протокол сессии переведён на скилл
crewsforge-session: бизнес-бриф с гейтом до захвата, без отдельной спеки, без остановки на плане, решения реализации проверяет субагент-оппонент, ревью одно итоговое с двумя тирами.
Сверка проектных документов на полноту
Заголовок раздела «Сверка проектных документов на полноту»Сопоставлены 32 модели, 10 машин, 4 консьюмера и 6 поверхностей шагов 2–7 со списком эпиков шага 9. Три расхождения, все внесены в step-документы:
- Эпик «Домен Team» отсутствовал в шаге 9. Спроектирован в шаге 2 §3, присутствует в таблице
задач шага 7 (
TEAM_ONBOARDING) и вSubjectType, но эпика не имел — при том что от него зависят оффер, подписант сcanSign, сдача сcanSubmitи выплатной аккаунт. Заведена единицаT1; шаг 9 дополнен эпиком F6. - Машина статуса команды не имела таблицы переходов. Объявлена в
T2:ONBOARDING → ACTIVE ⇄ SUSPENDED → ARCHIVED. ExternalPaymentDisputeне объявлен в схеме шага 2. Требуется шагом 5 §6; модель вводитD3.
Отклонения от проектных документов
Заголовок раздела «Отклонения от проектных документов»- Шаг 9 дополнен разделом «Дополнение 2026-08-30» (эпик F6, машина команды, модель chargeback’а)
и разделом «Актуальная нарезка исполнения», отсылающим к
ROADMAP.md. Волны и критический путь шага 9 остаются основанием, но исполнение идёт по срезам.
Ошибка этой сессии — и что из неё следует
Заголовок раздела «Ошибка этой сессии — и что из неё следует»Сессия сначала прочитала ROADMAP.md и SESSION-LOG.md из рабочего дерева, находясь на ветке
feat/f1-shared-core-libs, созданной до закрывающих коммитов F1. На той ветке F1 всё ещё
IN_PROGRESS, а закрывающей записи нет — они в main. В результате сессия сообщила Stark’у, что
F1 не закрыта, и построила первую версию перекроя поверх устаревшей копии: слияние откатило бы
F1 в IN_PROGRESS и удалило бы 60 строк закрывающей записи. Исправлено перебазированием на
main до коммита.
Следствие для протокола: фаза 0 скилла обязана читать контур с main, а не из рабочего
дерева, и останавливаться, если текущая ветка не main. Требование усилено в
crewsforge-session явной командой сверки.
Перенумерация единиц
Заголовок раздела «Перенумерация единиц»Хендофф F1 ссылается на старые ID. Соответствие:
| Старый ID | Новый ID | Что переехало |
|---|---|---|
F4 baseline-схема | F2 | сверка Prisma-enum’ов с union’ами domain-events, дельты TimerKind и SubjectType |
F2 либа outbox | T2 | outbox вместе с executor’ом машин |
B2.1 Hold и Checkout | M2 | конвертация bigint → number для Stripe |
C4.1 операторское место | M3 | денежный линтер и depConstraints на границу api-core-money |
B3.1 Settlement | M4 | founderRefundMinor → колонка founder_amount_minor |
A3 спор и расторжение | D1 и D2 | допущение об одном shareBps в splitSettlement |
F2 (подграф core в mermaid) | T2 | подграф добавляется вместе с первым ребром |
Заглушки и долги
Заголовок раздела «Заглушки и долги»- Заглушек нет — единица документарная.
- Открытый долг из
F1:docs/DOC.zipдостижим из коммита327c31fв истории смерженной ветки. Вычистить можно только переписыванием истории — решение Stark’а, не сессии. - Открытый долг из
F1: у либlibs/apis/core/нет таргетовbuildиtypecheck, поэтому компиляционные гейтыdomain-eventsподключены к спеке side-effect-импортом. Единице, которая заведёт настоящийtypecheck, стоит проверить, не стал ли приём лишним.
Хендофф следующей сессии
Заголовок раздела «Хендофф следующей сессии»- Следующая единица —
F2(целевая схема, сиды, платформенные роли). Зависимостей нет,F1закрыта. Обязательна сверка enum’ов с@crewsforge-back/apis/core/domain-events. - Первая единица с фронтовой поверхностью —
T1(команды). Она же закрывает дыру шага 9. - Сессия запускается командой
/crewsforge-session; она сама остановится на бизнес-брифе.
F2 — Целевая схема, сиды, платформенные роли
Заголовок раздела «F2 — Целевая схема, сиды, платформенные роли»Открыта: 2026-08-30 · Статус записи: в работе Зависимости на момент захвата: нет (F1: DONE) Ветка: feat/f2-baseline-schema
Бизнес-бриф (утверждён Stark’ом)
Заголовок раздела «Бизнес-бриф (утверждён Stark’ом)»Задача: база перестаёт быть базой прошлой версии продукта — в ней появляются все 32 модели сделочного контура (команды, план, этапы, холды, расчёты, споры, расторжения, очередь задач, таймеры, уведомления, консьерж). Платформа получает три рабочих места вместо одного «админа»: оператор, администратор, арбитр — с запретом совмещения. Логин под каждым работает, и по токену видно, куда пускать.
Сущности. Все из шага 2, группами:
- Команда:
Team,TeamMember(осиrole⟂canSign/canSubmit),TeamInvitation - Проект и план: переработанный
Project(новыйProjectStatus10 значений,contractingStage, валюта, бюджетные вилки,commissionRateBps),Specification,PlanVersion - Работа:
Milestone,AcceptanceCriterion,Artifact,MilestoneSubmission,AcceptanceReview,ReviewRejectionItem - Контрактинг:
Party,PartyDocument,Contract(+templateVersion/documentHash, шаг 6 §2),Offer - Деньги:
PayoutAccount,Hold,Settlement(денормализованныйhold_amount_minorпод CHECK),WebhookEvent - Спор и расторжение:
Dispute,DisputePosition,DisputeEvidence,Termination - Операционный контур:
StateTransition,OutboxEvent,QueueItem,Timer,Notification - Консьерж и трекер:
ConciergeThread,ConciergeMessage,TrackerLink - Роли:
RoleTypeрасширяется наOPERATORиARBITER
API для фронта:
| Метод | Путь | Кто вызывает | Назначение | Возвращает |
|---|---|---|---|---|
| POST | /api/v1/auth/admin/login + /login/verify | сотрудник платформы | существующий 2FA-логин, теперь кладёт в JWT claim platformRole | пара токенов; в payload platformRole: OPERATOR|ADMIN|ARBITER |
| GET | /api/v1/operator/me | оператор | кто я и какое у меня рабочее место | { userId, email, platformRole: OPERATOR } |
| GET | /api/v1/administrator/me | администратор | то же для администратора | { ..., platformRole: ADMIN } |
| GET | /api/v1/arbiter/me | арбитр | то же для арбитра | { ..., platformRole: ARBITER } |
Плюс: существующие защищённые маршруты admin-api (/api/v1/projects, /api/v1/users) получают
AdministratorGuard — до этого туда пускал любой валидный admin-токен, что с появлением
оператора и арбитра стало бы дырой O-1.
Можно собрать на фронте: экран входа в бэк-офис и роутинг после логина — три рабочих места по
одному ответу /me, с честными 403 при попытке зайти не в своё.
Чего ещё нельзя: ничего доменного. Ни команд, ни проектов нового вида, ни этапов, ни очереди
задач, ни денег — таблицы существуют пустыми, писать в них некому до T1. ProjectStatus в базе
меняется на новый набор из 10 значений: ломающее изменение, заявленное шагом 8.
Правки скоупа при утверждении: скоуп не менялся. Stark подтвердил три решения, вынесенные в
бриф: (1) тесты по реальной БД гоняются на локальном docker-контейнере PostgreSQL, который сессия
поднимает сама; (2) billing-api и админка появятся сильно позже, поэтому запрет ArbiterGuard
в billing-api доказывается тестом «нигде, кроме admin-api» с расширением в T3; (3) навешивание
AdministratorGuard на существующие админские маршруты фронт не ломает — админки ещё нет.
Закрыта: 2026-08-30 · Статус записи: закрыта · PR: #31
Сделано
Заголовок раздела «Сделано»- Целевая схема шага 2 одной baseline-миграцией.
apps/core-api/prisma/schema.prisma— 61 модель (30 прежних + 31 новая) и 42 enum’а; 14 прежних миграций схлопнуты в20260830000000_baseline. Дельты на месте:templateVersion/documentHash(шаг 6 §2),ConciergeThread/ConciergeMessage(шаг 7 §5), частичный индекс дедупаQueueItem(шаг 7 §2). - Четыре инварианта на уровне БД:
settlements_amounts_sum_to_hold(CHECK),queue_items_open_dedup,users_on_roles_single_platform_role,roles_type_key. Последний добавлен по итогам ревью: без уникальности типа роли предыдущий индекс обходился второй строкойrolesтого же типа с другим id. - Политика удаления:
Restrictна всей сделочной цепочке,Cascade— только у профильных сателлитов, сессий, приглашений и тредов Walrider; список каскадов закреплён allow-list-тестом. - Сиды baseline: пять ролей по детерминированным id, три платформенных пользователя, фаундер,
команда с разведёнными осями
role⟂canSign/canSubmit, два проекта (DRAFT,TEAM_MATCHING). Сиды идемпотентны и подхватывают пользователя, уже занявшего фикстурный email. - Три платформенные роли в admin-JWT: claim
platformRoleвыводится изUserOnRole, перевыводится при каждом выпуске пары, включая refresh, и сверяется с базой на каждом запросе — снятая роль перестаёт действовать сразу, а не через 15 минут. - Три guard’а (
OperatorGuard,AdministratorGuard,ArbiterGuard) и три маршрутаGET /api/v1/{operator|administrator|arbiter}/meв новой библиотекеadmin-api-feature-workspace. Существующие/api/v1/projectsи/api/v1/usersзакрытыAdministratorGuard. - Снята старая машина статусов проекта (подтверждено Stark’ом):
PROJECT_STATUS_TRANSITIONSобеих копий,ProjectUpdateStatusDto, founder-маршрутPATCH .../status, полеstatusвAdminProjectUpdateDto. Возвращается переходами машины вP1. - Обвес тестов по живой БД:
docker-compose.test.yml(PostgreSQL 16 на 55432 + Redis на 56379), npm-скриптыtest:db:up/down, таргетыintegrationуcore-api-e2eиadmin-api-e2e, таргетflowsуadmin-api-e2e, таргетtestуcore-apiс зависимостью отprisma:schema:generate.
Решения реализации
Заголовок раздела «Решения реализации»Полный блок — в плане. Проверен субагентом
crewsforge-decision-reviewer в два круга: первый вернул 6 возражений (все приняты, блок
переписан), второй — 4 пропуска (логин не-ADMIN, потеря claim’а при refresh, unit-спеки снятой
поверхности, свежесть сгенерированного клиента); все закрыты до реализации.
Ключевые: squash в один baseline (решение №31) · @@map и @map по coding-rules §1.11 ·
запрет совмещения платформенных ролей индексом БД, а не только сервисом (решение №19) · claim несёт
значения RoleType (ADMIN), карта PLATFORM_ROLE_BY_ROLE_TYPE переводит их в PlatformRole
схемы (ADMINISTRATOR) · конфиг-модуль параметров платформы не заводится — нет потребителя ·
сверка enum’ов сравнивает три источника (текст схемы, сгенерированный клиент, union’ы шины).
Приёмка
Заголовок раздела «Приёмка»| Критерий карточки | Чем доказан |
|---|---|
migrate reset даёт целевую схему, validate зелёный | prisma migrate deploy на чистом контейнере + migrate diff --from-url … --to-schema-datamodel → -- This is an empty migration. |
| Удаление проекта со сделочной цепочкой падает в БД | deal-chain-deletion.integration.spec.ts — P2003 с именем FK, строка остаётся |
Settlement не сходится в холд → отказ БД | settlement-amounts.integration.spec.ts — raw INSERT мимо сервиса, ассерт на имя констрейнта |
| Кросс-ролевые 403; вторая платформенная роль не выдаётся | platform-role-access.integration.spec.ts (матрица 3×5 по живому HTTP), platform-role-uniqueness.integration.spec.ts |
| Сверка enum’ов роняет прогон при расхождении | apps/core-api/prisma/schema-enums.spec.ts — 12 утверждений; доказано мутацией схемы и протухшим клиентом |
ArbiterGuard не регистрируется вне admin-api | arbiter-guard-isolation.spec.ts — краснеет при добавлении импорта в другой сервис (проверено) |
| Чек-лист шага 8 §3 | инвариант-тесты ✓, SQL-проверка отсутствия CASCADE ✓ (deal-chain-foreign-keys, cascade-allowlist), удаление старой машины статусов ✓; пункт «журнал + outbox на смоук-переходах» невыполним до T2 — машин ещё нет |
HTTP-e2e: admin-api-e2e:integration — 35 тестов, включая сквозной вход оператора, администратора
и арбитра из сидов через настоящий Redis и выдачу пары, и матрицу доступа. core-api-e2e:integration
— 28 тестов. admin-api-e2e:flows — 7. Swagger admin-api содержит все три маршрута брифа.
Ревью: усиленный тир, два crewsforge-unit-reviewer параллельно. Оба вернули FAIL — 22 находки,
20 исправлены (в том числе три дыры: claim жил до истечения токена, индекс платформенных ролей
обходился второй строкой справочника, сид падал на занятом email). Две отклонены с обоснованием —
см. «Отклонённые замечания» в теле PR.
Отклонения от проектных документов
Заголовок раздела «Отклонения от проектных документов»- Шаг 8 §3 правлен: удаление
PROJECT_STATUS_TRANSITIONS/ProjectUpdateStatusDto/ PATCH-маршрутов статуса перенесено из эпика A1 (=P1) вF2. Основание: смена enum’а не оставляет старой таблице переходов возможности скомпилироваться. Подтверждено Stark’ом 2026-08-30. - Шаг 8 §2, «Конфиги» не исполнен: карточка
F2конфигов не называет, потребителя в единице нет, календарь уже живёт константойapi-core-platform-calendar(F1). Параметры приезжают со своими потребителями (O1,M2,M4); таблицы под них не заводилось — её нет в схеме шага 2. - Шаг 4 §1 против шага 2 §9: claim несёт
ADMIN, enum схемы —ADMINISTRATOR. Документы не правились: множества разные по назначению (токен противQueueItem.targetRole), перевод делает карта вshared, тип claim’а названPlatformRoleClaim, чтобы имена не сталкивались. - Представление денег в HTTP-ответах — целое число минорных единиц. Правила в step-документах
нет; введено по подтверждению Stark’а 2026-08-30 после того, как
GET /api/v1/projectsначал отдавать 500 наBigInt.
Заглушки и долги
Заголовок раздела «Заглушки и долги»GeoSeederне работает: внешний источникrestcountriesv3.1 объявлен устаревшим и отдаёт заглушку вместо массива. Сбой справочников больше не роняет прогон (роли и фикстуры создаются), вdevelopmentкод возврата 0, на прочих окружениях — 1.TODO(F2)вinit.seed.ts. Снимет — единица, которой понадобятся гео-справочники (P1), либо отдельная задача на миграцию источника.- Границы инвариантов БД названы честно в
docs/05-data/database-schema.md:CHECKсверяет расчёт с денормализованнымhold_amount_minor, а не с холдом; число расчётов по холду не ограничено; actor-ссылки (executed_by_user_idи др.) — uuid без FK. Всё это транзакционные инвариантыM4/M5. apps/core-api/.env.developmentуказывает на удалённую базу (188.137.179.235:6432).nx run core-api:prisma:seed:runбез явногоPOSTGRES_URLзасеет её, и теперь ещё и dev-фикстурами. Решение — за Stark’ом.- Открытый долг из
F1не снят:docs/DOC.zipдостижим из коммита327c31f.
Хендофф следующей сессии
Заголовок раздела «Хендофф следующей сессии»- Следующая готовая единица —
T1(команды: состав, приглашения, полномочия). ЗависимостьF2закрыта:Team,TeamMember,TeamInvitationесть в схеме, роли и guard’ы работают, dev-фикстура команды с разведёнными осями полномочий уже в сидах. - Прогон
nx affected -t lint,test,buildкрасный по предсуществующим долгам, не по этой ветке: часть проектов имеет таргетtestбез единого спека и падает с «No tests found» (проверено наmain), остальное — нарушения границ Nx (admin-project,admin-user,admin-api-e2e,user-api-feature-*↔auth-api-feature-*). Ветка их не добавила и уadmin-projectуменьшила число ошибок с 7 до 1. Разгрести стоит отдельной задачей: пока прогон красный, он не сигнал. T3обязана расширитьarbiter-guard-isolation.spec.tsявным упоминаниемbilling-api, когда сервис появится: сейчас тест доказывает «нигде, кроме admin-api».api-core-moneyиспользует BigInt-литералы, поэтому любой проект, чей спек её импортирует, обязан иметьtarget: es2021в своёмtsconfig.spec.json(база держитes2015). Уже поднято уprisma-client,workspace,token, обоих*-e2e.- Интеграционные прогоны не входят в
affected— их надо гонять руками:npm run test:db:up+nx run core-api-e2e:integration+nx run admin-api-e2e:integration.
T1 — Команды: состав, приглашения, полномочия
Заголовок раздела «T1 — Команды: состав, приглашения, полномочия»Открыта: 2026-08-30 · Статус записи: в работе Зависимости на момент захвата: F2: DONE Ветка: feat/t1-teams
Бизнес-бриф (утверждён Stark’ом)
Заголовок раздела «Бизнес-бриф (утверждён Stark’ом)»Задача: сотрудник регистрирует команду, зовёт людей по email, раздаёт им доступ и отдельно —
право подписывать договоры и сдавать работу; администратор платформы верифицирует новые команды.
После единицы команда существует как субъект: у неё есть состав и известно, кто вправе подписать
договор и сдать этап. На это опираются оффер (P2), контрактинг (P3–P5), сдача (M3) и
выплатной аккаунт (T3).
Сущности:
Team— имя, уникальный слаг, публичное описание, внешний опыт (externalExperience), статусONBOARDING → ACTIVE, штампverifiedAt.TeamMember— членство с двумя независимыми осями:role(ADMIN/EDITOR/VIEWER) — доступ внутри команды;canSign/canSubmit— полномочия по 2.2 ФТ.VIEWERсcanSign: true— легальная комбинация. Ушедший участник →status: REMOVED, строка не удаляется.TeamInvitation— приглашение на email: одноразовый токен, срок, статусыPENDING/ACCEPTED/DECLINED/EXPIRED/REVOKED.
API для фронта:
| Метод | Путь | Кто вызывает | Назначение | Возвращает |
|---|---|---|---|---|
| POST | /api/v1/teams | employee | создать команду; создатель — ADMIN с обоими полномочиями | карточка команды |
| GET | /api/v1/teams/:teamId | участник | профиль команды | карточка |
| PATCH | /api/v1/teams/:teamId | ADMIN команды | правка имени, описания, внешнего опыта | карточка |
| GET | /api/v1/teams/:teamId/members | участник | состав с ролями и полномочиями | список членств |
| PATCH | /api/v1/teams/:teamId/members/:memberId | ADMIN команды | сменить роль и/или canSign/canSubmit | членство |
| DELETE | /api/v1/teams/:teamId/members/:memberId | ADMIN команды | убрать участника (→ REMOVED) | 204 |
| DELETE | /api/v1/teams/:teamId/members/me | участник | выйти из команды | 204 |
| GET | /api/v1/teams/:teamId/invitations | ADMIN команды | приглашения команды, фильтр по статусу | список |
| POST | /api/v1/teams/:teamId/invitations | ADMIN команды | пригласить по email с назначенной ролью | приглашение + токен |
| DELETE | /api/v1/teams/:teamId/invitations/:invitationId | ADMIN команды | отозвать (→ REVOKED) | 204 |
| GET | /api/v1/team-invitations/:token | employee | что за приглашение: команда, роль, срок | краткая карточка |
| POST | /api/v1/team-invitations/:token/accept | employee | принять | созданное членство |
| POST | /api/v1/team-invitations/:token/decline | employee | отклонить | 204 |
| GET | /api/v1/employees/me/teams | employee | мои членства (мультичленство разрешено) | список |
| GET | /api/v1/administrator/teams | администратор | список команд, фильтр по статусу, пагинация | { data, meta } |
| GET | /api/v1/administrator/teams/:teamId | администратор | карточка команды с составом | карточка |
| POST | /api/v1/administrator/teams/:teamId/verify | администратор | ONBOARDING → ACTIVE, штамп verifiedAt | карточка |
Первые 14 — в user-api (решение №08: домен команд — либы team-* в user-api). Последние три —
в admin-api под префиксом администратора (шаг 4 §8), отдельная либа-фича поверх того же домена.
Можно собрать на фронте: онбординг команды целиком — создание, профиль с правкой, экран состава с матрицей «роль × canSign × canSubmit», отправка и отзыв приглашений, страница приёма приглашения по ссылке, переключатель между своими командами. Админский экран: список команд с фильтром «на верификации» и карточка с кнопкой «верифицировать».
Чего ещё нельзя: письмо с приглашением не уходит (рассылка — O2; токен возвращается в ответе
и передаётся приглашающим вручную) · задача TEAM_ONBOARDING администратору не заводится (очередь —
O1; новая команда видна фильтром status=ONBOARDING) · статус меняется прямо, без журнала
переходов и outbox, реализован единственный переход ONBOARDING → ACTIVE (SUSPENDED/ARCHIVED
и журнал — T2) · команда не подключает выплаты (T3) и не получает офферы (P2).
Правки скоупа при утверждении: скоуп не менялся. Stark задал вопрос о размещении домена —
почему либы в user-api, а не отдельный team-api; после разбора оснований (одна БД на
монорепозиторий, employee-JWT и EmployeeGuard уже в user-api, guard «активное членство» в горячем
пути авторизации, седьмой деплоймент при нулевом сегодняшнем выигрыше) решение №08 подтверждено
без изменений: выделение в отдельный сервис — после беты, триггер «свой релизный цикл или своё
масштабирование».
Закрыта: 2026-08-31 · Статус записи: закрыта · PR: #34
Сделано
Заголовок раздела «Сделано»user-api-feature-team— доменное ядро команд:TeamService(создание с членством создателя, профиль, список с пагинацией, верификация),TeamMemberService(состав, две независимые оси, удаление, выход, реактивация ушедшего),TeamMembershipGuard+ декоратор@TeamRoles, три контроллера (teams,teams/:teamId/members,employees/me/teams).user-api-feature-team-invitation— приглашения: криптотокен, ленивое истечение, адресность приёма, пипа токена, контроллерыteams/:teamId/invitationsиteam-invitations/:token.admin-api-feature-admin-team— поверхность администратора поверх того же доменного ядра: список с фильтром и пагинацией, карточка с составом, верификацияONBOARDING → ACTIVE.user-api-data-access— DTO и константы домена; либа получила теги Nx, благодаря чему admin-api переиспользует её DTO вместо копии.api-shared— валидаторIsAtLeastOneFieldDefined(пустое телоPATCH— 400).apps/user-api-e2e— обвяз интеграционных прогонов (таргетintegrationпо образцуF2) и два доменных спека;apps/admin-api-e2e— спек админской поверхности.
17 маршрутов из утверждённого брифа на месте, все в Swagger.
Решения реализации
Заголовок раздела «Решения реализации»Полный блок — в плане. Проверен субагентом
crewsforge-decision-reviewer в два круга: первый вернул три возражения по существу (скоуп
вложенных ресурсов :memberId/:invitationId, контракт externalExperience, видимость строк
REMOVED в списках) — все приняты и закрыты; второй — CONFIRMED по 52 решениям.
Ключевые: домен — либы в user-api (решение №08), админская поверхность переиспользует то же ядро ·
403 за :teamId, 404 за вложенный ресурс своей команды · выборка вложенных ресурсов только по паре
(teamId, id) · инвариант последнего администратора под блокировкой строки команды
(SELECT … FOR UPDATE) · подбор слага снаружи транзакции · ленивое истечение приглашений ·
приглашение адресное на приёме и отклонении, предпросмотр открыт держателю токена.
Приёмка
Заголовок раздела «Приёмка»| Критерий карточки | Чем доказан |
|---|---|
Сквозной HTTP-e2e: создать → пригласить → принять → выдать canSign → отказ последнему админу → убрать → строка REMOVED | team-lifecycle.integration.spec.ts — 10 тестов по живой БД, шаг в шаг по карточке |
Оси независимы: VIEWER с canSign: true | тот же спек (выдача canSign не трогает роль) + карточка команды в administrator-team.integration.spec.ts |
| Приглашение по истёкшему или использованному токену отвергается | team-access.integration.spec.ts: 410 + перевод строки в EXPIRED (в том числе при приёме), 409 на принятом и отозванном, 404 на неизвестном |
Чужой teamId — 403, а не 404 | матрица «9 маршрутов × чужая команда» в team-access.integration.spec.ts; доказана мутацией (замена на 404 роняет 9 тестов) |
| Swagger содержит все маршруты; примеры в PR | дамп OpenAPI обоих сервисов; пары запрос-ответ в теле PR |
HTTP-e2e: user-api-e2e:integration — 50 тестов, admin-api-e2e:integration — 58 (включая 35 из F2).
Unit: 263 теста в пяти проектах. nx affected -t lint,test,build красный на 16 задачах —
ровно тех же, что падают на чистом main (проверено прогоном в отдельном worktree); ветка не
добавила ни одной новой.
Ревью: обычный тир, один crewsforge-unit-reviewer (СООТВЕТСТВИЕ) — PASS с десятью находками.
Восемь исправлены, две закрыты документацией; ни одна не отклонена. Существенные: утечка пути к
файлу и модели Prisma при нечисловом teamId, выдача полномочий участнику со статусом REMOVED,
Swagger админского списка объявлял массив вместо { data, meta }, отсутствие 409/404 в контракте
маршрутов состава.
Отклонения от проектных документов
Заголовок раздела «Отклонения от проектных документов»- Шаг 4 §7 даёт администратору платформы «R/W (онбординг, передача admin)» — передача роли
в
T1не реализована: это расширение поверхности против утверждённого брифа. ПомеченоTODO(T2)вadmin-team.controller.tsи описано вdocs/03-services/admin/admin-api.md. Step-документ не правился: возможность отложена, а не отменена. - Шаг 9, дополнение 2026-08-30 объявляет машину
ONBOARDING → ACTIVE ⇄ SUSPENDED → ARCHIVED. ВT1реализован единственный переходONBOARDING → ACTIVEпрямой записью — так сказано в карточкеT1(«журнал придёт вT2»); расхождения с документом нет.
Заглушки и долги
Заголовок раздела «Заглушки и долги»- Письмо с приглашением не отправляется (
TODO(O2)вTeamInvitationService.saveOne): токен возвращается в ответе на создание и в списке приглашений, фронт передаёт ссылку вручную. СниметO2. - Задача
TEAM_ONBOARDINGадминистратору не заводится (TODO(O1)вTeamService.saveOne): очереди задач нет. Новая команда видна фильтромstatus=ONBOARDING. СниметO1. - Передача роли администратора команды из бэк-офиса —
TODO(T2), см. «Отклонения». PrismaExceptionFilterотдаёт сыройexception.messageв fallback-ветке (например дляP2023) — это утечка внутренностей на любом маршруте любого сервиса, где сырой запрос получает битый идентификатор. ВT1закрыто на входе guard’ом; сам фильтр — предсуществующий долгshared.- Три markdown-файла (
docs/03-services/user/user-api.md,docs/03-services/admin/admin-api.md, README либы приглашений) переформатированы prettier’ом: репозиторий markdown не форматирует, и около 200 строк их дифа — переносы, а не смысл.
Хендофф следующей сессии
Заголовок раздела «Хендофф следующей сессии»- Следующая готовая единица —
T2(машины состояний и события). ЗависимостиF1иT1закрыты.T2обязана: перевести смену статуса команды на журнал переходов и outbox (сейчас прямая запись), добавить переходыSUSPENDED/ARCHIVEDи снятьTODO(T2)вadmin-team.controller.ts(передача роли администратора команды). - Контракты, на которые можно опираться:
TeamCoreModuleэкспортируетTeamServiceиTeamMemberService— это единственная точка изменения состава;TeamMembershipGuard+@TeamRoles(...)закрывают любой маршрут вида/teams/:teamId/...; полномочияcanSignиcanSubmitуже разведены с ролью и ждут потребителей вP5иM3. - Полномочия читать только у активных участников: строки
REMOVEDостаются в базе и хранят последние значенияcanSign/canSubmitна момент ухода. Любой потребитель обязан фильтровать поstatus: ACTIVE— домен свои записи закрыл, но чужие выборки об этом не знают. - Интеграционные прогоны не входят в
affected, гонять руками:npm run test:db:up+nx run user-api-e2e:integration+nx run admin-api-e2e:integration+nx run core-api-e2e:integration. - Теги Nx проставлены у
user-api-data-access. Уauth-api/features/tokenи остальных либ их по-прежнему нет — из-за этого admin-api не может импортироватьtoken, иAdminTokenPayloadберётся изapi-shared. Разгрести стоит отдельной задачей.
T2 — Машины состояний и события
Заголовок раздела «T2 — Машины состояний и события»Открыта: 2026-08-31 · Статус записи: в работе Зависимости на момент захвата: F1: DONE, F2: DONE, T1: DONE Ветка: feat/t2-state-machines
Бизнес-бриф (утверждён Stark’ом)
Заголовок раздела «Бизнес-бриф (утверждён Stark’ом)»Задача: администратор получает полный жизненный цикл команды — верифицировать, приостановить с указанием причины, вернуть в строй, архивировать. Каждое действие оставляет неудаляемую строку журнала (кто, когда, на каком основании) и в той же транзакции порождает доменное событие в outbox. Команда видит, что приостановлена, и по какой причине. Механизм, делающий это атомарно и без гонок, строится здесь и принимается на первой настоящей машине, а не на синтетическом тесте.
Сущности: новых моделей и миграции нет — F2 уже завёл обе таблицы.
StateTransition— журнал переходов, append-only; пишет только либаstate-machine.OutboxEvent— доменное событие в той же транзакции; никем ещё не читается.- Машина статуса команды
ONBOARDING → ACTIVE ⇄ SUSPENDED → ARCHIVED. - Новый routing key
team.status.changedв каталоге событийF1:SubjectType.TEAMв каталоге есть, ключа для команды нет, и компиляционный гейтCatalogCoversEveryRoutingKeyзаставляет добавить его осознанно.
API для фронта:
| Метод | Путь | Кто вызывает | Назначение | Возвращает |
|---|---|---|---|---|
| POST | /api/v1/administrator/teams/:teamId/verify | администратор | ONBOARDING → ACTIVE — существует с T1, переписывается на машину | карточка команды |
| POST | /api/v1/administrator/teams/:teamId/suspend | администратор | ACTIVE → SUSPENDED, reason обязательна | карточка команды |
| POST | /api/v1/administrator/teams/:teamId/reinstate | администратор | SUSPENDED → ACTIVE, reason опциональна | карточка команды |
| POST | /api/v1/administrator/teams/:teamId/archive | администратор | → ARCHIVED из любого статуса, reason обязательна | карточка команды |
| GET | /api/v1/administrator/teams/:teamId/transitions | администратор | история: откуда, куда, кто, когда, основание, причина | { data, meta } |
| POST | /api/v1/administrator/teams/:teamId/members/:memberId/grant-admin | администратор | выдать роль ADMIN активному участнику (снимает TODO(T2) из T1) | карточка участника |
| GET | /api/v1/teams/:teamId | участник команды | расширяется блоком suspension: { reason, since } | null | карточка команды |
Коды отказов, единые для всех переходов: 409 TRANSITION_CONFLICT (состояние не то либо кто-то
опередил), 422 с кодом guard’а, 403 ACTOR_NOT_ALLOWED с записью попытки в журнал
(metadata.denied).
Можно собрать на фронте: админский экран команды целиком — четыре кнопки действий с формой причины, состояние кнопок выводится из текущего статуса, вкладка «История» с лентой «администратор X приостановил 12 августа, основание: …», передача роли администратора команды. В интерфейсе команды — баннер приостановки с причиной и датой.
Чего ещё нельзя: события ложатся в outbox и никем не читаются — relay и консьюмеры в O1;
письмо о приостановке не уходит (O2); журнал переходов не отдаётся ни команде, ни фаундеру —
вообще ни одним клиентским маршрутом (L-4, решение №19); задача TEAM_ONBOARDING по-прежнему не
заводится (O1); приостановка технически ничего не блокирует, кроме самой себя — блокировать
нечего, проектов и денег ещё нет.
Правки скоупа при утверждении: сессия вынесла на гейт три развилки, Stark согласился с рекомендацией по каждой.
- Архивация из любого статуса, а не только из
SUSPENDED. Шаг 3 и карточка дают цепочкуONBOARDING → ACTIVE ⇄ SUSPENDED → ARCHIVED, из которой буквально следует архивация только изSUSPENDED; тогда брошенную на онбординге команду-спам убрать нечем. Реализуется один переход сfrom: [ONBOARDING, ACTIVE, SUSPENDED]— ни один инвариант не ослаблен. - Передача роли admin команды взята в скоуп — расширение поверхности против карточки
T2, но прямое требование шага 4 §7 и незакрытыйTODO(T2)изT1: команда, потерявшая единственного администратора, сейчас нередактируема навсегда. - Резолвер стороны сделки не реализуется, объявляется только вариант
{ side, authority }в типеActorSpec. У машины команды актора-стороны нет — все переходы делает администратор платформы; реализация без потребителя непроверяема. Резолв приедет сP1/P2.