# Journey: Public scene · anonymous abuse report → operator triage

This record follows the shipped report path for a public Living Offering. The
public form, `202` receipt, operator-list projection, reporter read-back, and
report-specific resolve/dismiss APIs are real. The join stops at two important
boundaries: reports live in a process-local singleton with no snapshot sink, and
the operator console's visible controls write to generic incident stores rather
than the report-specific resolution endpoints.

## Personas

- **Anonymous viewer** — reports a public or revoked `/scene/<shortCode>` link
  without needing an account.
- **Signed-in reporter** — submits the same form and can later read their
  receipt on `/profile/safety`.
- **Trust-and-safety operator** — sees the report projected into
  `/operator/admin` and opens the affected offering.
- **API operator** — calls the report-specific resolve or dismiss endpoint; this
  is the only shipped way to mutate the report's own status.

## Pre-conditions

- A Living Offering share exists. The report endpoint looks up the share by
  short code but does not require it to remain active.
- The public form at `/scene/<shortCode>/report` offers the fixed reasons
  `privacy`, `self-harm`, `harassment`, `copyright`, `misinformation`, and
  `other`. Detail is optional and capped at 2,000 characters.
- Public abuse protection permits at most 120 requests in 60 seconds for this
  route group.
- A bearer token is optional. When one is valid, the BFF derives the reporter
  identity from it; the request-body id is not authoritative for that case.

## Steps

### 1. Open and submit the public form

`PublicSceneReportForm` preserves the offering back link, requires one reason,
allows an empty detail, and posts to
`POST /v1/living-scenes/public/:shortCode/report`. The page exposes an explicit
failure status when the request is rejected and a success message only after an
OK response.

### 2. Record and route the report

The BFF validates the short code, normalizes the reason/detail, records the
report, appends `living_scene.public_report_created` to the admin audit stream,
and returns `202` with the report receipt. Every reason uses the same routed
queue, `lilith-living-scene-public-reports`; `self-harm` becomes severity S1,
harassment/copyright/misinformation S2, and privacy/other S3 in the admin-list
projection. Severity is not a separate crisis-review workflow.

### 3. Inspect the operator projection

`GET /v1/admin/abuse-reports` maps reports to `INC-*` rows with category `auto`,
the calculated severity, report status, and a `/scene/<shortCode>` deep link.
`/operator/admin` prepends those rows to `AdminInboxConsole`, where the operator
can inspect the summary and open the offering.

### 4. Resolve through the report API

`POST /v1/admin/abuse-reports/:id/resolve` and `/dismiss` update the report
record and append `living_scene.public_report_resolved` or
`living_scene.public_report_dismissed` audit events. A signed-in reporter reads
the resulting status through `GET /v1/living-scenes/public-reports/mine`.

The visible `AdminInboxDecisionPanel` and `AdminInboxIncidentActions` do **not**
call those endpoints. They update the generic operator-decision and incident
lifecycle stores, so using them does not resolve or dismiss the abuse report.

### 5. Apply a separate takedown when warranted

The Living Offering deletion/takedown API can tombstone public URLs and revoke
download grants, and browser coverage proves that cascade. It is a separate
operator/API action; resolving an abuse report does not automatically take the
offering down, warn a sharer, or suspend an account.

## Post-conditions

- The submitter receives a `202` report receipt and, when authenticated, can
  read its current status on `/profile/safety`.
- The operator list contains an incident-shaped projection with the correct
  severity and offering deep link.
- Resolve/dismiss status and actor/note are readable for as long as the BFF
  process remains alive; corresponding admin audit events are appended.
- No claim is made that the report itself, generic operator decision, incident
  action, and Living Offering takedown are one atomic lifecycle.

## Failure modes

- **Process restart** — `livingSceneAbuseReportStore` is a module singleton over
  an in-memory map. It has no durable snapshot wiring, so report-list and
  reporter-receipt state disappear on restart even when admin audit persistence
  is configured.
- **False operator closure** — generic verdict/incident buttons can succeed
  while the abuse report remains `open`; only the report-specific API changes
  that record.
- **Revoked share** — a known revoked share still accepts a report and returns
  `202`; this is intentional coverage, not proof that the share is playable.
- **Missing detail** — valid and accepted. A reason is the minimum useful
  payload.
- **Unknown report id** — resolve/dismiss returns `404` and creates no terminal
  record.
- **Rate limit** — the 121st request in the covered window returns `429`.
- **Unavailable BFF** — the public form keeps the viewer on the page and shows
  “Report could not be sent”; there is no offline report queue.

## E2E coverage

- [`apps/oshun/web/e2e/public-scene-abuse-report.spec.ts`](../../apps/oshun/web/e2e/public-scene-abuse-report.spec.ts)
  drives the form constraints and failure state, anonymous `202`, optional
  detail, S1 projection, API resolve/dismiss read-back, unknown-id rejection,
  and the signed-in `/profile/safety` receipt.
- [`apps/oshun/web/e2e/scene-password-and-grants.spec.ts`](../../apps/oshun/web/e2e/scene-password-and-grants.spec.ts)
  covers report creation on active and revoked shares, routed-queue output, rate
  limiting, and the separate deletion/tombstone/download-grant cascade.
- **Coverage depth: partial.** The API lifecycle is deep; the missing browser
  join is a report-specific resolve/dismiss/takedown control in the operator UI.

## Per-view files touched

- [`customer/12-scene/scene-id-report.md`](../customer/12-scene/scene-id-report.md)
  — public form and submit states.
- [`customer/12-scene/scene-id.md`](../customer/12-scene/scene-id.md) — affected
  offering and tombstone result.
- [`customer/09-account/profile-safety.md`](../customer/09-account/profile-safety.md)
  — authenticated reporter receipts.
- [`operator/operator-admin.md`](../operator/operator-admin.md) — operator
  projection and the generic-control boundary.

## Cross-references

- [`scene-public-viewer-anonymous.md`](./scene-public-viewer-anonymous.md) — the
  public surface from which the report link opens.
- [`scene-keep-and-share.md`](./scene-keep-and-share.md) — share and revocation
  lifecycle.
- [`incident-triage.md`](./incident-triage.md) — generic operator incident
  lifecycle, which is adjacent but not report-specific.
- Sources: `apps/oshun/web/src/app/scene/[id]/report/PublicSceneReportForm.tsx`,
  `apps/oshun/bff/src/living-scenes/abuse-report-store.ts`, and
  `apps/oshun/bff/src/routes/living-scenes.ts`.

## Open questions

- Which operator control should call report-specific resolve/dismiss, and should
  it optionally compose with a Living Offering takedown?
- Which durable store should own abuse reports and reporter receipts across
  restart?
- Should S1 self-harm reports route to a distinct safety queue rather than only
  receiving a higher severity label in the shared queue?
- What explicit account/sharer enforcement actions, if any, belong in this
  workflow?
