---
status: walked + e2e-covered
coverage_depth: deep
last_walked:
  '2026-07-16 by Codex — reconciled the fixture-backed operator inbox, live
  public-report union, durable incident lifecycle store, per-operator decision
  record, current deep links, and browser/server rationale gates.'
specs:
  - apps/oshun/web/e2e/incident-triage.spec.ts
  - apps/oshun/web/e2e/operator-inbox-decision-bff.spec.ts
---

# Journey: Incident triage

An operator opens an incident, filters and selects it, records a review
decision, applies shared lifecycle actions, follows any available evidence link,
and resolves the record with an audited rationale. The current implementation
has two related but different data planes:

- `/operator/admin` renders a six-item editorial/tenant/auto fixture unioned
  with live public-scene abuse reports. Its queue filters and selection are
  interactive.
- `/v1/admin/operator-incidents` owns the shared lifecycle state for the six
  seeded `INC-2036`…`INC-2041` records. It supports acknowledge, assign,
  escalate, snooze, and resolve, with durable snapshot wiring when the admin DB
  is configured.

`AdminInboxDecisionPanel` is a third, per-operator verdict record. It is useful
for review accountability but does not replace the shared incident lifecycle.
Background incident creation, realtime/on-call delivery, a complete downstream
routing matrix, SLA timers, and automatic queue removal after resolve remain
outside the shipped browser path.

## Personas

- **House operator** — filters, selects, assigns, and resolves the queue.
- **Crisis-trained reviewer** — target owner for crisis-class S1 work; routing
  and paging are not yet wired here.
- **Domain reviewer** — receives editorial, evidence, tenant, or abuse context
  when an explicit deep link is available.
- **On-call operator** — target recipient for SLA escalation; the current store
  contains no paging adapter.

## Pre-conditions

- The operator is authenticated with an accepted admin/operator scope.
- `/operator/admin` can load its fixture and any available public-report union.
- The incident lifecycle store is registered. In a deploy with an admin DB,
  `wireDurableOperatorIncidents` hydrates and write-through persists its full
  status, assignment, and history snapshot.
- The audit-event store is reachable; every successful lifecycle verb records a
  discrete `incident.*` event.
- Six seeded lifecycle incidents align by ID, severity, title, and category with
  the six base inbox rows. Live public reports do not automatically become rows
  in that lifecycle store.

## Steps

### 1. Incident enters the current catalog

- [x] The base inbox contains six seeded incidents: one S1, two S2, and three S3
      rows across Auto, Editorial, and Tenant categories.
- [x] Live public-scene abuse reports are transformed into additional Auto-lane
      rows; self-harm maps to S1 and each row carries its public scene short
      code.
- [ ] A background producer creates an arbitrary new shared lifecycle incident
      through a production ingestion route. The lifecycle store is seeded and
      exposes no create endpoint.
- [ ] An S1 creation pages on-call and emits a realtime operator notification.

### 2. Open and orient in `/operator/admin`

- [x] The page identifies Lilith Admin / House / Inbox and shows open/critical
      fixture counts.
- [x] All, Editorial, Tenant, and Auto are real button filters.
- [x] Each row exposes code, severity, time, actor, title, subtitle, and
      category through stable DOM anchors.
- [x] The initially selected drift incident paints its detail card, source rows,
      per-operator decision panel, and shared lifecycle actions.
- [x] Anonymous access redirects before the operator console renders.

The header count is derived from the loaded inbox catalog. It does not currently
subscribe to lifecycle mutations, so resolving an incident in the action panel
does not remove its fixture row or decrement the header in the same view.

### 3. Filter and select

- [x] All renders the complete union.
- [x] Auto, Editorial, and Tenant filter by each row's real category field.
- [x] Selecting a row updates the detail card, severity, evidence blocks,
      decision target, and lifecycle-action target.
- [ ] Filter selection is encoded in the URL for reload/share continuity. It is
      component-local today.
- [ ] Severity ordering is enforced as a queue sort invariant for live unioned
      rows. The base fixture order is authored, not recomputed by the client.

### 4. Record a per-operator review verdict

- [x] Choose a verdict in `AdminInboxDecisionPanel`.
- [x] Enter a rationale of at least 20 trimmed characters; shorter input cannot
      submit.
- [x] Record the decision through
      `POST /v1/admin/operator-inbox/:itemId/decision`.
- [x] The response exposes a real audit ID; GET readback rehydrates the decision
      and reload preserves the visible state.
- [x] Reopen is a real BFF operation for this decision record.
- [x] Missing auth, insufficient scope, malformed verdict, and short rationale
      fail without a fabricated success.

This verdict is scoped to the operator and inbox item. Shared status changes are
performed by the next step.

### 5. Apply shared lifecycle actions

- [x] **Acknowledge** records the acting operator once. A second acknowledge
      receives 409 `ALREADY_ACKNOWLEDGED`.
- [x] **Assign** requires a non-empty assignee and appends an `assigned` history
      row.
- [x] **Escalate** only raises severity (`S3→S2` or `S2→S1`) and requires a
      rationale of at least 50 characters. S1 exposes no higher target.
- [x] **Snooze** offers 1h, 4h, and 24h in the UI. The server forbids S1, caps
      S2 at 24h, and caps S3 at 7d.
- [x] **Resolve** requires one of `actioned`, `no-violation`, `duplicate`,
      `referred`, or `wont-fix` plus a 50-character rationale.
- [x] Every successful verb appends shared history and records a corresponding
      audit event (`incident.acknowledged`, `.assigned`, `.escalated`,
      `.snoozed`, or `.resolved`).

### 6. Follow the evidence link that actually exists

- [x] A unioned public-report row links back to `/scene/<shortCode>`.
- [x] A row with `provenanceHref` exposes its explicit provenance link.
- [ ] Tenant, Studio, Veritas, Nyx, and appeal rows all deep-link into a
      context-preserving resolution surface. Those links are not present for
      every fixture class today.
- [ ] Every destination provides “Back to incident” with the incident ID and
      current filter preserved.

### 7. Resolve and verify readback

- [x] Select a non-terminal incident, choose a resolution class, supply at least
      50 characters, and submit.
- [x] The action panel shows `data-incident-status="resolved"` only after the
      real BFF response succeeds.
- [x] The shared store rejects a second resolve with 409 and keeps the first
      resolution canonical.
- [x] With durable snapshot binding, the shared status/history survives a store
      restart and rehydrate.
- [ ] The browser refetches the shared incident after reload and removes it from
      the default open queue. Current E2E verifies the write response; the
      fixture-backed queue is not driven by that store readback.

## Post-conditions

- The selected incident's shared lifecycle store contains every successful
  mutation in order and, after resolve, a terminal `resolved` state and
  resolution class.
- Each successful lifecycle verb has an audit-event record; rejected requests
  produce no mutation event.
- Any per-operator verdict remains separately readable with its own audit ID.
- The visible action panel never invents a status after a 400, 403, 409, or
  service failure.
- Queue removal, SLA metrics, on-call escalation, and downstream-action foreign
  keys are not post-conditions of the current runtime.

## Failure modes

- [x] **Concurrent acknowledge** — the second writer receives 409; the first
      actor remains canonical.
- [x] **Short escalation or resolution rationale** — UI disables submit and the
      server independently returns 400 `RATIONALE_TOO_SHORT` with no mutation.
- [x] **Non-monotonic/top-severity escalation** — the store rejects the change.
- [x] **Invalid resolution class** — the store rejects the change.
- [x] **S1 snooze / excessive duration** — server policy rejects it.
- [x] **Missing or insufficient operator scope** — the BFF returns 401/403 and
      the browser reports an honest error.
- [x] **Network split** — no optimistic status is painted; the user can retry.
- [ ] **Duplicate producer fires** — no ingestion/debounce path currently
      increments an occurrence count.
- [ ] **Realtime channel loss** — there is no missed-window reconciliation
      contract for newly created incidents in this surface.
- [ ] **SLA breach** — no timer or paging adapter is connected to the lifecycle
      store.
- [ ] **Broken audit/provenance chain** — define the exact fail-closed boundary
      without blocking unrelated publishes globally.

## E2E coverage

- [`apps/oshun/web/e2e/incident-triage.spec.ts`](../../apps/oshun/web/e2e/incident-triage.spec.ts)
  covers the real-BFF `/operator/incidents` feed, `/operator/admin` anatomy,
  exact base rows, filters, selection reactivity, public-report union,
  decision-panel audit receipt, assignee persistence, acknowledge → escalate →
  resolve, anonymous redirect, and short-rationale server rejection.
- [`apps/oshun/web/e2e/operator-inbox-decision-bff.spec.ts`](../../apps/oshun/web/e2e/operator-inbox-decision-bff.spec.ts)
  covers decision create/read/reopen, payload/auth gates, and per-operator
  isolation.
- Store and route tests cover assign/snooze policies, terminal transitions,
  audit emission, and durable snapshot hydrate/write-through.
- **Coverage depth:** deep for the existing queue/selection/decision/lifecycle
  core. Incident ingestion, paging/realtime delivery, full deep-link routing,
  live queue readback after lifecycle mutation, SLA automation, and producer
  dedupe remain uncovered product legs.

## Per-view files touched by this journey

- [`operator/operator-admin.md`](../operator/operator-admin.md) — filterable
  inbox, selected detail, decisions, and lifecycle actions.
- [`operator/operator-overview.md`](../operator/operator-overview.md) — console
  entry.
- [`shell/04-auth-session.md`](../shell/04-auth-session.md) — operator gate.
- [`shell/05-notifications.md`](../shell/05-notifications.md) — target realtime
  and paging behavior.
- [`operator/operator-studio.md`](../operator/operator-studio.md),
  [`operator/operator-tenant.md`](../operator/operator-tenant.md), and
  [`operator/operator-admin-isis-provenance.md`](../operator/operator-admin-isis-provenance.md)
  — partial/future resolution destinations.

## Cross-references

- [`citation-drift-resolution.md`](./citation-drift-resolution.md) — the
  evidence-resolution branch.
- [`public-scene-abuse-report.md`](./public-scene-abuse-report.md) — a live
  producer unioned into the browser inbox.
- [`user-flag-to-review.md`](./user-flag-to-review.md) — customer report origin.
- [`V1/features.md`](../../V1/features.md) — Trust and Safety, severity/SLAs,
  and audit explorer requirements.
- Current UI: `apps/oshun/web/src/components/lilith/AdminInboxConsole.tsx`,
  `AdminInboxDecisionPanel.tsx`, and `AdminInboxIncidentActions.tsx`.
- Current routes/stores:
  `apps/oshun/bff/src/routes/admin-operator-incidents.ts`,
  `apps/oshun/bff/src/routes/admin-operator-inbox.ts`, and
  `apps/oshun/bff/src/admin/operator-incident-store.ts`.

## Open questions / known gaps

- [ ] Add a production incident-ingestion contract and dedupe/recurrence model.
- [ ] Make the browser inbox list/counts read shared lifecycle status and
      refetch after a mutation.
- [ ] Complete and test the context-preserving deep-link matrix.
- [ ] Define the realtime snapshot/reconnect channel and on-call paging adapter.
- [ ] Implement SLA clocks, breach events, and post-incident-review obligation.
- [ ] Decide how live public-report union rows become shared lifecycle records
      before exposing lifecycle controls for those IDs.
