# Journey: User report → Trust & Safety review

This is an authenticated customer-report flow, not an anonymous incident
pipeline. Six customer surfaces share one report dialog and BFF intake. The
admin Trust & Safety page can read the resulting queue, while the moderation
decision currently happens through an admin API rather than a browser form. The
reporter then receives an in-app decision message and a safety receipt.

| Stage               | Shipped boundary                                        |
| ------------------- | ------------------------------------------------------- |
| Customer intake     | Shared dialog on six signed-in surfaces                 |
| Report store        | Bounded, process-local array in the BFF                 |
| Admin browser       | Read-only Trust & Safety report roster                  |
| Moderation decision | Privileged API with five recorded outcomes              |
| Customer notice     | In-app message and `/profile/safety` receipt            |
| Enforcement         | No content or account mutation is invoked by this route |

## Personas

- **Signed-in customer** — reports an item and can read only their own receipt.
- **Trust & Safety admin** — reads the moderation workspace and, through the
  API, records a decision.
- **Customer message center** — delivers the decision notice and tracks
  delivery, read, and click receipts.
- **Reported creator or artifact owner** — contextual subject only; this route
  does not notify or mutate that account.

## Pre-conditions

- The customer has an authenticated session. `POST /v1/user-reports` derives the
  reporter id from that session; there is no anonymous intake path.
- The customer is on one of the six mounted origins:
  - Veritas lead article;
  - Veritas claim detail;
  - Nisaba passage;
  - Tara sitting;
  - Metis lesson;
  - Metis tutor.
- The origin supplies stable target kind, id, and label metadata.
- The admin decision caller has `admin:*`, `admin:studio`, `admin:moderation`,
  or `admin:workspace:support`.
- The same BFF process remains alive if later read-back of the submitted report
  is required.

## Steps

### 1. Customer opens the shared report dialog

- [x] `TrustSafetyFlagButton` exposes a minimum 44 px Report action with an
      accessible target label.
- [x] The dialog presents the ten canonical categories: `harassment`, `hate`,
      `impersonation`, `misinformation`, `spam`, `nsfw`, `violence`,
      `self_harm`, `privacy`, and `other`.
- [x] Rationale is required and constrained to 10–500 characters.
- [ ] The form does not collect a consented context bundle, scroll position,
      screenshot, attachment, or appeal intent.

### 2. Customer submits through the same-origin route

- [x] The browser posts to `/api/trust-safety/flags` so the access-token cookie
      is not exposed to client JavaScript.
- [x] The route forwards the request to `POST /v1/user-reports`.
- [x] The BFF validates category, target kind, target metadata, and rationale,
      and takes `reporterId` from the authenticated request context.
- [x] A new report starts as `open`, priority `medium`, outcome `none`, and
      unassigned.
- [x] The dialog renders a submission acknowledgement and the customer can read
      the receipt from `GET /v1/user-reports/mine`.
- [ ] No classifier assigns P0–P3 severity, detects brigading, or routes a
      crisis-trained reviewer.

### 3. The report appears in the admin workspace

- [x] `recordCustomerUserReport` prepends the record to the admin moderation
      workspace's `userReports` projection.
- [x] `/trust-safety` renders that roster with the report's target, reporter,
      category, rationale, priority, status, and outcome.
- [ ] The array is module-level, bounded, and process-local. It is not a durable
      case-management ledger.
- [ ] The admin page is read-only for these rows. It has no assignment,
      evidence, decision, second-review, or appeal form.
- [ ] Customer reports do not become `INC-*` records in the Admin Cockpit
      incident workspace.

### 4. An authorized admin records an API decision

- [x] `POST /v1/admin/user-reports/:reportId/decision` accepts one of five
      outcomes: - `warned`; - `content_removed`; - `account_suspended`; -
      `policy_referral`; - `dismissed_no_action`.
- [x] The decision reason is constrained to 10–1000 characters.
- [x] The report is updated in the process-local roster and a
      `trust_safety.user_report.decided` admin audit event is recorded.
- [ ] Outcome names are moderation receipts, not proof of enforcement:
      `content_removed` does not remove an artifact and `account_suspended` does
      not change an account in this route.
- [ ] There is no browser decision control, two-reviewer gate, repeat-offender
      escalation, takedown cascade, or optimistic-concurrency token.

### 5. The reporter receives the decision

- [x] The decision route emits an in-app incident message for the original
      reporter.
- [x] The message deep-links to `/profile/safety?reportId=<id>`.
- [x] The reporter sees an actioned receipt on `/profile/safety` and the
      notification in `/messages`.
- [x] Following the deep link records a clicked receipt; read state remains a
      separate action.
- [x] Another customer cannot read the reporter-scoped receipt.
- [ ] No external push, email, reported-creator notice, or appeal is emitted.

## Post-conditions

- The authenticated reporter has a report id, current status, and eventual
  in-app decision receipt.
- The Trust & Safety roster and audit search expose the recorded decision for
  the lifetime of the BFF process.
- The decision is an administrative classification only; downstream content
  removal, account suspension, referral handling, and appeals remain outside
  this implementation.
- No documentation claim treats the read-only admin page as a complete
  moderation cockpit.

## Failure modes

- **Anonymous submit** — the BFF rejects the request; reporter identity cannot
  be supplied by the body.
- **Invalid target or category** — validation fails before the report is
  recorded.
- **Rationale outside 10–500 characters** — intake is blocked.
- **Missing admin scope** — the decision route rejects the caller.
- **Unknown report id or invalid outcome** — no report is changed and no
  decision audit event is written.
- **BFF restart** — the module-level report array and its decisions disappear.
- **Outcome mistaken for enforcement** — the route records `content_removed` or
  `account_suspended` but invokes no artifact or account mutation.
- **External delivery expected** — only the in-app message-center path exists.
- **Report from an unsupported surface** — no shared Report affordance is
  mounted there.

## E2E coverage

- [apps/oshun/web/e2e/user-flag-to-review.spec.ts](../../apps/oshun/web/e2e/user-flag-to-review.spec.ts)
  verifies all six mount points, target metadata, dialog accessibility, the
  ten-category picker, 10–500 character rationale gate, one real submission,
  admin workspace read-back, privileged API decision, admin audit search,
  reporter isolation, the actioned safety receipt, message deep link, and
  clicked receipt.

Coverage is **deep for the current single-process intake and receipt chain**. It
does not browser-drive a moderation decision because no such admin form exists,
and it does not prove durable storage, severity routing, enforcement, external
delivery, or appeal.

## Per-view files touched by this journey

- [customer/05-veritas/veritas-home.md](../customer/05-veritas/veritas-home.md)
  — Veritas lead report origin.
- [customer/06-tara/tara-sit.md](../customer/06-tara/tara-sit.md) — Tara sitting
  report origin.
- [customer/07-nisaba/nisaba-passage.md](../customer/07-nisaba/nisaba-passage.md)
  — Nisaba passage report origin.
- [customer/08-metis/metis-lesson.md](../customer/08-metis/metis-lesson.md) —
  Metis lesson and tutor report origins.
- [customer/09-account/profile-safety.md](../customer/09-account/profile-safety.md)
  — reporter-scoped decision receipt.
- [customer/02-home-discovery/messages.md](../customer/02-home-discovery/messages.md)
  — in-app decision notification.
- [ADMIN_WALKTHROUGH/trust-safety.md](../../ADMIN_WALKTHROUGH/trust-safety.md) —
  read-only admin Trust & Safety roster.

## Cross-references

- [trust-safety-voice-abuse-response.md](../../ADMIN_WALKTHROUGH/journeys/trust-safety-voice-abuse-response.md)
  — a separate admin policy-response journey.
- [incident-handling-admin.md](../../ADMIN_WALKTHROUGH/journeys/incident-handling-admin.md)
  — the dedicated incident cockpit; customer reports do not enter that model.
- `apps/oshun/web/src/components/TrustSafetyFlagButton.tsx` — shared dialog and
  validation UX.
- `apps/oshun/bff/src/routes/user-reports.ts` — customer intake and receipt
  routes.
- `apps/oshun/bff/src/admin/routes/user-reports.ts` — privileged decision route.
- `apps/oshun/bff/src/admin/state.ts` — bounded, process-local roster
  projection.

## Open questions

- [ ] Which durable case store should replace the module-level report array?
- [ ] Which admin surface will own assignment, evidence review, decisions, and
      second-review gates?
- [ ] Which service turns recorded decision outcomes into idempotent content and
      account enforcement?
- [ ] How will severity, crisis escalation, brigading controls, and
      repeat-offender rules be applied?
- [ ] Which decisions are appealable, and where does the reporter or reported
      creator enter that flow?
- [ ] Which external channels should supplement the in-app message?
