Appearance
Investigations
An investigation is the phase of a report's lifecycle during which an assigned agent works the case. It begins with the investigate action and ends with validate, invalidate, or conclude. The authoritative state machine lives in rpc_report_action (migrations 006 → 034); the application-side rules live in reports_service.perform_report_action and reports_service.conclude_investigation.
Lifecycle
text
Open / Pending ──(investigate)──► Under Investigation
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
validate invalidate conclude
▼ ▼ ▼
Validated Invalidated Pending
(closes contact thread, (same side effects) (no bot work, no queue row;
queues bot action) timeline + event only)Beginning an investigation
- Endpoint:
POST /reports/<id>/actionwith{"action": "investigate"}— the single application path.POST /reports/<id>/investigation/beginis a legacy adapter over the same operation. - Authorization:
manage_dockets(clearance 2+), supervisor-report rank check, and the assigned-agent rule (the acting agent must be assigned to the case, or hold Senior Agent rank or above). The bot key may also drive it. - Preconditions: the case must not already be
Under Investigation(409). Starting fromOpenorPendingis always allowed. Reopening a concluded case (Closed,Completed,Validated,Invalidated) requires clearance 4+; the timeline event reads "Investigation reopened". - Side effects: status →
Under Investigation, "Investigation begun" timeline row, canonicalreport.investigation_startedevent, and apending_actionsrow for the bot (the bot performs Discord-side work such as role changes and logs).
Working the case
While the investigation is in progress, the assigned agent (or supervisor rank) may:
- add notes (
POST /reports/<id>/notes) — required to be in this status for browser sessions; - add evidence (
POST /reports/<id>/evidence) — same gate; - add timeline entries (
POST /reports/<id>/timeline) — same gate; - message the reporter via the contact thread.
Notes and evidence are recorded through rpc_add_note / rpc_add_evidence, which write the row, the timeline entry, and the canonical event (report.note_added / report.evidence_added) in one transaction.
Completing the investigation
Three terminal paths, all of which first lock the report row and re-check the status inside the database:
| Path | Endpoint | Result | Queue row | Contact thread |
|---|---|---|---|---|
| Validate | POST /reports/<id>/action {"action":"validate"} | Validated | yes | closed |
| Invalidate | POST /reports/<id>/action {"action":"invalidate"} | Invalidated | yes | closed |
| Conclude | POST /reports/<id>/investigation/end, or PATCH status="Pending" while under investigation | Pending | no | unchanged |
Validate/invalidate require the current status to be exactly Under Investigation — otherwise 409. They write "Report validated/invalidated" and "Investigation concluded" timeline rows plus the matching canonical events, close any open contact thread, and queue the bot action. Conclude performs the status guard, writes "Investigation concluded" + report.investigation_concluded (metadata.to = "Pending"), and inserts no queue row — concluding carries no bot work.
Requeueing
There is no dedicated "requeue investigation" operation. Two recovery paths exist:
- A concluded case (
Pending) can be re-investigated with theinvestigateaction — the normal reopen path. - Failed Discord-side bot work associated with an action is recovered through the action queue:
POST /actions/<id>/requeue(failed → pending, attempts reset) or the admin queue controls. See Operations.
Concurrency behavior
Concurrency is handled inside the database, not in application code:
rpc_report_actionexecutesSELECT status, assigned_agent FROM reports WHERE report_id = … FOR UPDATEbefore any guard. Two agents racing to validate the same case: the first commits, the second finds the row alreadyValidatedand raisesSTATUS_CONFLICT→ HTTP 409 with "This action can no longer be performed — the case status changed".- The application pre-flight checks are duplicated in the RPC so the database is authoritative even if a route-level guard is bypassed.
- Claim (
action=claim) is guarded the same way:ALREADY_CLAIMED→ 409 when the case is already assigned.
Authorization requirements
| Requirement | Detail |
|---|---|
| Sign-in | Active browser session, or the bot API key for bot-driven actions |
| Clearance | manage_dockets (2) for action/begin/end; reassign_docket (4 + Head Investigator rank) for assignment changes |
| Supervisor reports | Senior Agent rank or above (or API key) |
| Assignment | Only the assigned agent may action/note/evidence/timeline/contact a case (supervisor ranks bypass) |
| Reopen concluded case | Clearance 4+ |
Authoritative database operations
| Operation | RPC | What it guards |
|---|---|---|
| Investigate / validate / invalidate / contact / claim | rpc_report_action | Row lock, allowed-status set, duplicate-contact guard, already-claimed guard; writes status/assignment + timeline + event + contact message + queue row atomically |
| Conclude | rpc_report_action (action conclude) | Status must be Under Investigation; timeline + report.investigation_concluded event, no queue row |
| Note / evidence | rpc_add_note / rpc_add_evidence | Row + timeline + event + updated_at in one transaction |
| Deletion | rpc_delete_report | Pre-image capture + delete + forensic audit in one transaction (see Reports) |