# #394 · Author handoff v2: публичная репутация до записи **Реализация разрешена Owner после этой целевой ревизии; новое design approval/micro-GO не нужно.** Автор — Cursor Auto в отдельной разрешённой рабочей копии; один глобальный writer. Canonical #394 / parent #372, основание trusted assignment и comment 6590824797. Продолжение в том же Issue/координированном PR с сохранением attempts/FAIL/UNKNOWN. Этот design-run не меняет product source, GitHub или runtime. База source `1d8a0c9b0115a499a786384c0f60da7de17c66d0`, supplied accepted DEV `7f33e9da0cf7b52d260567bbfb37114a3613d479`, alpha19/API0.27/`r41479055286-a2`. Trusted process фиксирует immutable revision авторского запуска и проверяет отсутствие второго writer. Дрейф базы требует адресного reconciliation, не нового изучения архитектуры. Результат одного PR/DEV delivery: доступный читатель без attendance читает отзывы из списка тренировок, игры и профиля; видит честное число held-игр организатора; подтверждённый участник отдельно пишет public текст; адресат отвечает/оспаривает; существующий Cases reviewer может принять решение. Нет реализации venue, общего Comments redesign, рейтинга навыка, новой auth/enrollment платформы, framework/scheduler, C5 или PROD. ## 1. Исторический предмет и единая проекция Точные существующие seams: - `apps/api/src/games/game-feedback.service.ts`: `ownEligibleParticipation`, `subjectHasConfirmedExperience`, `assertCurrentPeerCreateAdmission` — образцы строгой проверки. Broad `canCreateOrganization/Training` и `responsibleOrganizer()` не public gates: последний берёт текущего manager; `loadGameMeta()` читает текущего trainer. - `apps/api/src/games/game-attendance.policy.ts`: `eligibleFromTransitions`, `isPermittedPresentAssertion`; `game-attendance.service.ts` — missing/version0, own disputes/help. Самоотметку не принимать, новый present не закрывает открытый спор сам. - `apps/api/src/games/game-outcomes.{policy,service}.ts`: явный held, server end time, lifecycle. Сейчас outcome source manager_explicit; case_reviewer correction требует узкого расширения. - `apps/api/src/games/games.{controller,service}.ts`: publish/getGame/listPublished и trainer snapshot. `game.organizer.id` — legacy entity ID, может быть null; не считать его user identity. `trainer.displayName` — не proof фактической услуги. - `apps/api/src/admin/attendance-disputes.service.ts`: повторить assignment/step-up/expected versions/COI и scoped read для узкой коррекции held/исторической роли; не расширять приватный доступ всем staff. Добавить в games domain небольшой `role-reputation.{service,policy,controller}.ts` и DTO (это **новые предложенные имена**, не существующие файлы), регистрацию в `games.module.ts` и следующую свободную адресную SQL migration + rollback в `apps/api/migrations/`. В одной migration можно создать необходимые таблицы; не назначать номер по памяти. Хранить event-role proof revisions: game_id, role organization/training, внутренний subject_user_id, version, planned binding/version at publish, actual-performed confirmation при held, provenance actor/source, связанные game/outcome versions и correction rationale. Привязку менять append-only с историей; непротиворечивый pre-event binding + явное подтверждение проведения роли даёт основание для новых событий. Отсутствие такой истории, спорная замена или попытка приписать себе старую встречу → Cases, не тихий snapshot сегодняшнего manager. Новый ответственный после игры не получает прежние отзывы. Независимое correction может восстановить историческую роль/held по доказательству, но не подделать private PG3 и attendance. Для старой игры без доказательства — missing_history, без массового backfill. Данные из демо не мигрировать. `heldGamesCount`: COUNT DISTINCT game_id по current accepted organizer proof; ends_at ≤ DB now; status in (published,completed), outcome=held; нет открытого held/role dispute. Все available confirmed history, regular + trainer_led, включая private/unlisted; ни future/draft/cancelled/unknown, ни количество отзывов, ни roster не влияют. Нет count по каждому manager. Отрицательное решение по роли/held исключает, исправление восстанавливает ровно один event. Обычная жалоба на review и спор отдельного attendance count не меняют. Принятый proof не перезаписывать односторонним edit manager; спорные основания исключены из числа до решения, но опубликованный review имеет отдельный lifecycle. Единый server projector `(subject, role, viewer, permittedSurface)` возвращает summary/count/safe reviews всем поверхностям. Batched SQL для карточек списка, не N+1 и не счёт по клиентской странице. Начать с on-read SQL и согласованного снимка в пределах ответа; нет materialized-count daemon. При модерации/withdraw/privacy changes инвалидировать локальные UI запросы. Не обещать snapshot across network requests; одинаковое состояние DB даёт одинаковую сводку. Count и list items одного ответа согласованы транзакцией/одним запросом; pagination фильтруется сервером, cursor не содержит private ключей. Использовать no-store как существующие endpoints, без shared cache приватного viewer context. ## 2. Хранение, API и Cases — тот же кандидат Отдельные от `game_reviews` public role-review slots/revisions и reply revisions. Внутренние поля: author, subject, role, private basis references/versions, text/topics, immutable projection hash + consent, status, approved revision, moderation case link, version/idempotency receipts. Unique slot `(author,subject,role)` и event fence `(author,game,subject,role)` сохраняются после withdrawal. Revision waiting не удаляет old approved revision. Reply bound to exact review revision; новая публикационная версия скрывает старый reply до нового подтверждения/решения. Собственная author history показывает все состояния; public только current published и разрешённое privacy. Предлагаемые узкие API (новые, сверить стиль контрактов при реализации): | Контракт | Действие и защита | |---|---| | `GET /v1/role-reputations/:roleHandle?cursor=...` | Без требования attendance; optional session по текущей поверхности. RoleHandle — opaque публичный идентификатор роли, выдаваемый только уже разрешённой game/profile projection; не userId и не attendance token. Сам по себе знание handle не авторизация: заново проверить видимость роли; для ограниченной game-role projection нужен разрешённый game context, без расширения профиля | | `GET /v1/me/role-reviews/eligibility?roleHandle=...` | Своя past-events projection: допустимые основания и безопасные причины отказа. Только own встречи, никаких чужих roster; клиент не задаёт recipient userId | | `POST /v1/me/role-reviews` | `{gameId, role, expectedBasisVersions, text, topics, consentProjectionHash}`; сервер выводит исторического subject. MutationRunner, cookie mutation/CSRF, Idempotency-Key; immutable revision + pending Case link атомарно | | `GET /v1/me/role-reviews` и `POST /v1/me/role-reviews/:id/commands` | Own-only state/receipt; typed submit_revision/withdraw/resubmit/appeal с expectedVersion. Consent обязателен при новой public редакции, withdrawal не требует действующего attendance. Свой private proof не отдавать через public GET | | `POST /v1/role-reviews/:id/reply-commands` | Только исторический subject именно этого отзыва; submit_revision/withdraw собственного ответа. Нельзя withdraw чужой отзыв. Consent/expectedReviewRevision/expectedVersion обязательны для подачи | | `POST /v1/role-reviews/:id/reports` | Authenticated permitted reader или адресат; exact review/reply revision, reason, explanation. Без требования attendance. Дедупликация/receipt; жалобы не меняют visibility. Appeal связан с прежним решением и сохраняет Case history | | `GET /v1/admin/cases/:id/role-review-source` | Новый scoped exact read: current assignment, permission, step-up, COI, linked revision. Показывает нужный текст и минимальный proof, не общую историю автора | | `POST /v1/admin/cases/:id/commands` | Расширить существующий strict union typed actions `role_review_decide`, `role_basis_resolve`; target review/reply, exactRevision, expectedCaseVersion/expectedResourceVersion и rationale. Decisions approve/retain/request_correction/remove/restore. Basis held/role corrections имеют свои expected versions и proof; не совмещать незаметно с content approval | Public DTO: `roleHandle`, role, permitted subject display, `heldGamesCount` (organization only), `countScope=confirmed_history`, `historyMayBeIncomplete=true`, `publishedReviewCount`, opaque projectionVersion, items `{reviewHandle,role,text,topics,authorLabel,experienceState,reply?,allowedActions}`, opaque nextCursor. Не публиковать author IDs/profile/avatar, game IDs, dates, private location/roster, Case IDs, reports count, private feedback/notes, reason/evidence или сырой cursor. В profile projection subject display ограничен её правами; game-role projection не становится полным профилем. Privacy role unavailable ≠ zero count. Own-only eligibility может иметь gameId и дату своей игры — это другой DTO и no-store. Все OpenAPI schemas strict (`additionalProperties:false`). `apps/api/src/safety/safety.service.ts`: добавить **отдельный** typed public review/reply report intake. Не снимать `kind === peer` и received-only ограничения существующего PR471 `reportReview/readReviewReport`. `apps/api/src/admin/cases.{controller,service,dto}.ts`, `apps/admin/src/components/Cases.tsx`: расширить существующую очередь pending submissions/replies, reports и appeals; durable Case/link создаётся атомарно с подачей без необходимости заранее иметь assignee. В общей очереди только минимальные metadata; source-text открывает назначенный независимый reviewer. Add filter по pending kind и явные buttons/confirmation + rationale, version conflict/UNKNOWN handling. `AdminApp.tsx` уже рендерит section=cases; использовать существующий developer Admin маршрут (basePath из `apps/admin/next.config.ts`), не выдумывать `/admin/reputation` и новые аккаунты. Первая линия: reuse validation/escaping, role admission, constraints/rate/idempotency; синхронные flags явных контактов, ссылок, подозрительных данных как подсказки reviewer. Не делать запрет критики по словам/тональности. Flag не human decision. Никакого paid model call. Pending недоступен публике даже при открытом Case. Report на опубликованный текст оставляет его видимым до scoped human remove; угрозы/PII требуют приоритетного отображения в существующей очереди, без выдуманного SLA. Reviewer может сразу скрыть точную редакцию мотивированным remove, позднее restore. Жалоба на reply не скрывает сам review. Automated checks не нажимают approve. COI: author, role subject, reporter, event participant, historical manager/creator/trainer и reviewer исходного решения для appeal исключены. Каждое чтение/команда перепроверяет текущий principal/session/step-up/assignment и COI; reassignment и потеря доступа очищают UI. Existing CaseEvidencePolicy/TTL не обходить: не создавать новый бессрочный evidence blob или general staff copy private PG3. Public revision и consent/proof refs в отдельном scoped review storage; scoped чтение источников соблюдает действующие evidence fences. Если источник больше нельзя прочесть, решение не выдумывать — held/needs_information, history сохраняется. Generic Case metadata status=resolved само по себе не публикует review. При approval/restore перепроверить consent, exact revision, eligibility/basis и current visibility; не публиковать withdrawn или старую редакцию. Для уже одобренного review одностороннее adverse manager change → basis-under-review + Case, не automatic removal; действительно закрывшаяся public visibility скрывает независимо от знака отзыва. Edit скрытого/снятого текста не оживляет его без нового согласия и решения. Existing UNKNOWN reconciliation сохранять, новый key не использовать для слепого повтора. ## 3. UI: до записи, затем свой опыт | Реальные файлы | Изменение | |---|---| | `apps/web/src/app/games/page.tsx`, `components/consumer/pages/GamesDiscoveryPage.tsx`, `components/consumer/discovery/DiscoveryGameCard.tsx` | Сейчас URL `/games?activity=trainer_led`, API `listGames({activityKind:'trainer_led'})`. Показать trainer identity + reviews entry прямо в карточке. Добавить узкий `trainer=` URL filter в тот же список и `trainerRoleHandle` query в `listGames`/GamesController/listPublished и `apps/api/src/games/games-discovery.query.ts` (normalizeDiscovery/eligibleGames), с existing discovery visibility/cursor filters. Это новый filter, не существующий trainer dashboard. Сохранять city/day/activity; handle не даёт private games. Переход из trainer profile ведёт сюда; пустой будущий список не обнуляет репутацию | | `components/consumer/pages/GameDetailPage.tsx`, `components/consumer/games/GameDetailComposition.tsx` (`GameTerms`) | Organizer count + тексты; trainer reviews. Сводка до join/booking CTA, доступна outsider. Не грузить own results ради чтения. Если субъект один, одна identity с двумя роль-входами | | `components/consumer/pages/PublicPlayerProfilePage.tsx`, `components/consumer/players/{PublicPlayerProfile,PlayerProfileContent}.tsx` | Ролевые разделы на `/players/[publicPlayerId]` из того же projector. Organizer без trainer flag тоже получает свой раздел при разрешённой роли. Self-declared trainer flag сам не даёт write eligibility | | `components/consumer/games/{GameFeedbackPanel,GameResultsComposition,GameReviewsHistory}.tsx`, `components/consumer/pages/GameResultsPage.tsx` | Private/public drafts отдельны, private default; public consent preview. Выбор своей eligible встречи из game/list/profile entry; нет опыта → читаем без принуждения писать. Pending receipt, edit/withdraw, appeal, own reply state. Missing/unknown/спор ведут к существующим уточнениям | | `apps/web/src/lib/{api-client,consumer-types,game-results}.ts`, `packages/contracts/openapi.json` | Согласованные DTO/клиент/контракт. `resultsPath(id,'feedback')` = `/my-games/{id}/results?section=feedback`. `/games/[gameId]/review` — заявки, НЕ отзывы; `/profile/trainer` — собственный редактор | Публичный review drawer/modal использовать как bounded reusable component с SurfacePanel/SectionHeader и существующим ProfileDialog/CloseButton, не gallery framework. Внутри видимый close, keyboard/focus return, mobile sheet, touch target ≥44px, Onest/текущие токены. Сохранить listing return context и chosen subject при переходе к composer. Текст и поля защищены от stale session response; drafts только память, без localStorage. UNKNOWN receipt не терять при очистке текста/смене сессии, не раскрывать новой сессии. `heldGamesCount`, published count и snippets не пересчитывать в UI. Loading/error/role unavailable/confirmed_history=0 имеют разные тексты. Не суммировать два role counts в рейтинг человека. Не печатать «15/20» как настоящую статистику. ## Проверки и единый выпуск Последовательные этапы 1→2→3 — **один coordinated PR/DEV delivery**, не независимые infrastructure/source-only releases. Автор до QA выполняет адресные deterministic проверки, переиспользует принятые проверки неизменённого кода: 1. Policy/SQL fixture: own held+present+historical role; self/outsider writer deny, outsider reader allow; future/cancelled/draft/unknown exclude; self-mark, substitution и open dispute hold; роль после смены не переносится. Count distinct stable при нескольких revisions/reviews и private held, не выводит детали; old missing proof не backfill. Исправление роли пересчитывает правильного subject. 2. Disposable HTTP на новых endpoints: exact consent и stale versions, duplicate/replay после потери допуска, private PG3 regression; public DTO/list/game/profile одинаковые counts и no private fields/cursors. Negative submit допускается; report не скрывает; human remove/restore и author-only withdraw, reply ownership/versions, appeal COI. Один scoped admin check assignment/step-up/COI loss; повторное generic status action не approve. Queue без assignee принимает pending, atomic failure не даёт fake success. 3. Targeted UI: training list→review→profile→own eligible composer; неподтверждённый читатель читает; private/public не перетекают, edit сбрасывает consent, pending без онлайн reviewer, organizer dispute и отдельный reply. 390/1440px, close/Escape/focus/overflow; fake app approval не human UAT. Полная ELIGIBILITY — контракт поведения, не требование построить отдельную синтетическую матрицу на каждый пункт. Только подтверждённый FAIL расширяет тесты; два прохода без улучшения → адресное evidence и новый подход, не слепой retry. Содержательный кандидат → **один независимый Codex Sol Medium QA другой модели** → обязательные Product CI/security → trusted merge dev → серверный DEV ACCEPTED + smoke. Runtime capability/миграция/Cases route должны работать; офлайн reviewer не блокирует intake. DEV smoke: submit→pending+receipt, permitted outsider read, Cases queue и отсутствие private утечки. Реальный approve/remove/restore в DEV — только уже существующая authorized session, иначе `human_moderation_UAT=NOT_TESTED` с конкретным недостающим наблюдением. HTTP role tests дают code evidence, не имитируют ручную приёмку. Не создавать сессии/credentials, не искать cookies. Не объявлять пользовательский сценарий полностью проверенным, если human часть не выполнена; при этом доставить готовую DEV очередь и сразу передать Owner UAT. Подготовить ограниченный явный DEV seed/fixture (distinct DEMO subjects, held-role proof, автор и предмет, негативный pending/published только с честным источником решения) существующим разрешённым способом. Ни присвоенных approvals, ни подмешивания вымышленных игр реальному организатору. Fixture readiness сообщить trusted process; если данные не готовы — NOT_TESTED с точным шагом, не claim ready. Release agent до выпуска готовит app version/описание; после ACCEPTED trusted integrator записывает exact source SHA, receipt/release_id, фактическую дату и UAT, обновляет существующую историю версий. API version отдельно. Source/QA/CI/DEV/human UAT/Owner UAT хранить раздельно; нет нового design micro-GO, нет PROD. ## Проверка design-артефактов Галерея v2 переиспользует inline assets/CSS/шаблоны v1, содержит восемь clickable DEMO-состояний. Проверено локально: JSON parse, 24 уникальных write-cases и отдельные read/count/moderation cases; JS syntax; генерация восьми состояний без duplicate IDs/undefined; отсутствие remote assets/network/storage code; HTML <3 MiB. Подставной DOM проверил offline pending, consent reset, отдельные drafts, запрет фиктивного human decision при offline, единые counts remove/restore в списке/игре/профиле, report без снятия, reply pending/approval и own withdrawal. Это проверка артефакта, не браузерного layout/accessibility. Browser visual QA = NOT_TESTED; продуктовые тесты, независимая QA, DEV и human/Owner UAT этим пакетом не заявляются. Product worktree после проверки clean, HEAD не изменён.