# #394 · bounded handoff: отзывы об организаторе и тренере **Предложение для следующего явно admitted source запуска. Сейчас — не команда на реализацию/merge/deploy.** Canonical #394, parent #372. Один writer, тот же Issue/PR и история попыток. Ни новой очереди, ни повторной PG1a/1b/2/3/4 реализации. База чтения: HEAD `1d8a0c9b0115a499a786384c0f60da7de17c66d0`, tree `5a82f31d9ca1f4a1f3b8bcd27e97bf919628178f`; supplied installed `7f33e9da0cf7b52d260567bbfb37114a3613d479`, alpha19/API0.27, receipt `r41479055286-a2`. Перед авторским запуском trusted process привязывает фактическую immutable revision. Данное предложение не изменяет runtime. ## Scope на первую поставку Добавить отдельные публичные тексты об организации/тренировке с opt-in, историческим адресатом, строгим опытом, премодерацией и одним ответом. На игре и существующем профиле — ролевой summary; писать/исправлять из собственных результатов. Без звёзд, общей оценки человека, автопубликации private PG3, venue writer, новых ролей, payments, scheduler или Radar-redesign. Defaults: private audience; отдельный пустой public draft; 1–500 plain text, без ссылок/вложений, ≤2 темы из трёх для роли; нейтральная подпись автора без профиля; один текущий public slot author/subject/role и уникальность основания author/game/subject/role; новые события обновляют слот. Нет срока давности/охлаждения, но всегда независимое решение точной версии. Public disabled при недоступной независимой обработке. Старый принятый текст при edit остаётся до нового решения, withdrawal скрывает сразу и не освобождает slot. ## Slice A — доказуемый предмет и допуск (public writer CLOSED) - `apps/api/src/games/game-feedback.service.ts`: `ownEligibleParticipation`, `subjectHasConfirmedExperience`, `assertCurrentPeerCreateAdmission` — образец блокировки и повторной проверки; переиспользовать нижележащие predicates, не вызывать broad `canCreateOrganization/Training` как public gate. Private методы и доступ к history не расширять. - `apps/api/src/games/game-attendance.policy.ts`: `eligibleFromTransitions` и `isPermittedPresentAssertion`; `game-attendance.service.ts` — existing missing/dispute/help. Version 0 отличается от existing unknown. Не сводить участие к текущему participation status. - `apps/api/src/games/game-outcomes.{policy,service}.ts`: конец + held отдельно от attendance. Сейчас source outcome — manager_explicit; independent outcome/role basis ещё не готов. - `game-feedback.service.ts`: `responsibleOrganizer` использует текущих active managers; `loadGameMeta` — текущий `trainer_profile_id`. `apps/api/src/games/games.service.ts` содержит trainer display snapshot, но это не удостоверение услуги. - Добавить только bounded event-role proof в games domain: exact event/version, исторический subject/role, provenance и correction history. Снимок из надёжной event-version истории; если её нет — новый явный независимый Cases outcome/role evidence path. Никакого массового backfill current manager/trainer в прошлое. Зафиксировать состав доказательства до допуска: game/outcome/attendance versions, participation proof, role binding/version. Для неподтверждённой замены trainer — hold. Клиент выбирает свою встречу и роль, не произвольный recipient userId. - `apps/api/src/admin/attendance-disputes.service.ts`: переиспользовать assignment, expected versions, независимый correction flow. Доработка held/role basis должна быть отдельным узким typed Case action с audit и COI, без универсального entitlement engine. Проверяющий также не может быть историческим тренером/адресатом. Если этот путь не вошёл в кандидат — зависимые случаи закрыты, public launch не готов. Минимальные проверки A: параметризованный policy-тест same-event held/present/nonself/role; disposable create + replay после смены допуска; missing/unknown/отказ до начала/после начала; self-mark и substitute trainer. Проверить независимую коррекцию и текущий open dispute (новый present не закрывает его сам). Один тест historical-role drift: новый manager/trainer не получает старый отзыв/ответ. Не запускать заново несвязанные PG сценарии. ## Slice B — отдельная публичная запись и Cases lifecycle (пока CLOSED) Внутри существующего games domain допустимы небольшой специализированный service/policy/DTO для role-experience, одна адресная migration и методы controller. Названия новых файлов/endpoint — implementation choice; их нет в текущем исходнике. Не добавлять public-поле к private row, которое случайно откроет старые тексты. Нужны отдельные public revisions/projection и private evidence references, consent hash/revision, status, approval actor/version и уникальные fences. Общий target user ID остаётся внутренним. - Seams: `apps/api/src/games/game-feedback.controller.ts`, `game-feedback.service.ts` (MutationRunner/version/idempotency), `apps/api/src/safety/safety.service.ts`, `apps/api/src/admin/cases.service.ts`, `apps/admin/src/components/Cases.tsx`. - `reportReview()` и `readReviewReport()` PR471 сейчас намеренно `kind === peer`, received-only с exact consent. Не удалять этот guard для всех видов. Добавить отдельный typed target публичного role review/reply, safe metadata и narrowly scoped read/decision. Public content ≠ право на private attendance/Case evidence. У private organization/training не появляется общее staff чтение. - Новая public submission направляет точную редакцию и минимальное основание назначенному независимому Cases reviewer. Нужен реальный receipt и обработчик; «help requested» не выдавать за assignment. Staff permissions/step-up/reassignment/COI проверяются на каждом действии; текущий case.manage сам по себе не открывает всё private evidence. - Approve/correction/remove/appeal связываются с ревизией. Reviewer не редактирует чужой текст. Reply может оставить только исторический subject в этой роли, один на review, с отдельными revision/consent/проверкой; при принятии существенной новой версии отзыва старый ответ скрыть до повторного подтверждения адресатом, не приписывать его новым словам. - После публикации adverse manager change не auto-delete: сохранить proof, private Case, публичный бейдж «Основание уточняется» до independent reaffirm/removal. Report count не hide. Actual privacy fences остаются обязательными. - `packages/contracts/openapi.json`, существующие DTO/types и API-client обновить согласованно; `additionalProperties: false` сохранить. Public DTO — только role/text/topics/author label/experience state/opaque review token/безопасный ответ/разрешённые действия. Ни game/date/roster/accountId/contact/profile link автора, ни decoded cursor с private keys. Минимальные проверки B: exact consent и stale edit; одинаковый/новый idempotency key не создают дубль, withdraw не освобождает слот; чужой reply/self-review отклоняется; assigned read после reassignment/COI/step-up loss закрыт; unknown без повторной отправки. Disposable HTTP: public list/read/profile summary не содержат private fields (включая ответы, счётчики и pagination); старый private feedback остаётся private. Report на негатив не скрывает, independent removal скрывает и уменьшает public count. Runtime capability OFF не принимает submit, edit/reply; withdraw/report/help остаются доступны по безопасности и правам. ## Slice C — три существующие поверхности и включение | Реальный файл/маршрут | Bounded изменение | |---|---| | `apps/web/src/components/consumer/games/GameDetailComposition.tsx` → `GameTerms`; `pages/GameDetailPage.tsx`; `/games/[gameId]` | Короткая сводка «Отзывы об организации» у organizer, «О тренировках» у trainer. Одна identity card с двумя ролями, если subject совпал. Q&A CommentsSection остаётся отдельно | | `apps/web/src/components/consumer/players/{PublicPlayerProfile,PlayerProfileContent}.tsx`; `pages/PublicPlayerProfilePage.tsx`; `/players/[publicPlayerId]` | Ролевой section только при server capability и сохранённой profile visibility. Нет общей «Оценки игрока», нет нового публичного trainer URL | | `apps/web/src/components/consumer/games/{GameFeedbackPanel,GameResultsComposition,GameReviewsHistory}.tsx`; `pages/GameResultsPage.tsx` | Выбор private/public без переноса текста, eligible composer, exact preview consent, pending/edit/withdraw и путь уточнения. Private sent/received list сохраняется; public status не выдаётся за private запись | | `apps/web/src/lib/{game-results,api-client,consumer-types}.ts` | Реальный `resultsPath(id, "feedback")` → `/my-games/{id}/results?section=feedback`; allowed actions с безопасными причинами. Свои подходящие встречи по серверной own projection, не загрузка чужого roster | | `apps/web/src/components/consumer/players/ProfileComposition.tsx`, consumer Button и текущие styles | Существующие ProfileDialog/CloseButton, focus/escape/return focus, 44px control, desktop Spacious40 и mobile sheet. Не копировать gallery shell как новый движок | Реальный `/games/[gameId]/review` — заявка/withdraw/refuse, **не** экран написания отзыва. `/profile/trainer` — собственный редактор. Публичный role summary не обходит expected private 404 и не раскрывает unlisted Game. UI guest/outsider может читать только доступную public projection, пишет после входа и проверки собственной встречи. При закрытой роли/профиле — недоступно, не «нет отзывов». При нуле — честный empty, при loading — loading. Отсутствие самостоятельного public organizer capability закрывает surface, а не создаёт новую роль. Минимальные UI checks: game→role→своя встреча→public preview; private/public drafts не перетекают; edit сбрасывает checkbox; closed writer/missing/disputed честны; privacy/session change очищает защищённый контекст и draft, UNKNOWN receipt не теряется. Одна identity с двумя ролями, без двойного счётчика «всего». Проверить 390 и 1440px, keyboard/focus/close, отсутствие горизонтального overflow. Данные только явно ограниченные DEV fixtures с известным состоянием, не private профили/cookies. ## Gates и handoff Авторские targeted deterministic checks ≠ независимая QA. Содержательный кандидат → один независимый reviewer другой модели по accepted маршруту → обязательные Product CI/security → trusted merge dev → server DEV ACCEPTED + smoke. До включения public writer проверить обслуживаемый независимый Cases путь, receipt, точное согласие и безопасное публичное чтение. Наличие source или зелёного exit не достаточно. При отсутствии обработки public writer CLOSED, без объявления DONE. После ACCEPTED trusted release agent фиксирует фактические app version, contract version, exact SHA, release_id/date/UAT в существующей истории версий. Номера следующей alpha/API здесь не назначены. Owner сразу получает входы и UAT=NOT_TESTED; ничего не публиковать от имени этого дизайнера. PROD/C5/проектная очередь не затрагиваются. ## Визуальная основа и проверка этого пакета Прочитаны scoped код, оба `docs/handoffs/reviews394-*.md`, прежние `reviews394-astra-policy-v1/{RECOMMENDATION,ELIGIBILITY,index}`, postgame372 design/contract и frozen Guide. Последний проверен по всем 48 manifest hashes. STYLE_GUIDE_PARITY: game identity→GameTerms; ролевой summary→SurfacePanel/SectionHeader; роль→TrainerProfileToggle badge family; состояния→postgame notice; composer→ProfileDialog + Spacious40; Onest, #f5f6f2/#13221a/#a7f447/#7945de, radius 12/18/28. Демонстрационные имена/тексты/счётчики явно DEMO; фото людей не используются. Выполнены локальные проверки: JSON parse и 22 уникальных случая матрицы; JS syntax; генерация HTML восьми состояний с проверкой IDs/labels; отсутствие remote assets/network/storage code; clean Git worktree и прежние HEAD/tree. Лёгкий прогон JS с подставным DOM проверил private score-only, закрытый public writer, сброс согласия при правке, сохранение адресата из профиля, pending и withdrawal. Этот прогон не проверяет браузерный layout или accessibility. Браузерный render/visual QA и продуктовые тесты не выполнялись: доступного браузера/Playwright нет. Это ограничение артефакта, не результат QA продукта. Галерея не отправляет данные, хранит демо только в памяти и сбрасывается при reload. Native dialog в галерее лишь показывает семейство overlay; production использует существующий компонент.