# Data Model: 111 — Findings Workflow V2 + SLA

**Date**: 2026-02-24  
**Branch**: `111-findings-workflow-sla`

---

## Modified Entities

### 1. `findings` Table (Lifecycle + SLA + Recurrence)

This feature evolves `findings` from v1 (`new|acknowledged|resolved`) to a v2 workflow and adds lifecycle metadata and SLA fields.

#### New Columns

| Column | Type | Constraints | Notes |
|--------|------|-------------|-------|
| `first_seen_at` | `timestampTz` | nullable initially; NOT NULL after backfill | Set on first observation; backfill from `created_at` where possible |
| `last_seen_at` | `timestampTz` | nullable initially; NOT NULL after backfill | Updated on every observation (including terminal findings) |
| `times_seen` | `integer` | default `0`, NOT NULL (after backfill enforce) | Incremented on every observation |
| `sla_days` | `smallint` | nullable | SLA policy value applied when `due_at` was set/reset |
| `due_at` | `timestampTz` | nullable | Only “open” findings participate in SLA due evaluation |
| `owner_user_id` | `bigint` | FK → users, nullable | Retained even if user is no longer a tenant member |
| `assignee_user_id` | `bigint` | FK → users, nullable | Retained even if user is no longer a tenant member |
| `triaged_at` | `timestampTz` | nullable | Set on `new|reopened → triaged` |
| `in_progress_at` | `timestampTz` | nullable | Set on `triaged → in_progress` |
| `reopened_at` | `timestampTz` | nullable | Set when transitioning into `reopened` (manual or automatic) |
| `closed_at` | `timestampTz` | nullable | Used for both `closed` and `risk_accepted` terminal outcomes |
| `closed_by_user_id` | `bigint` | FK → users, nullable | Actor for `closed` / `risk_accepted` |
| `closed_reason` | `string` | nullable | Reason required for `closed` and `risk_accepted` |
| `recurrence_key` | `string(64)` | nullable; indexed | Stable identity for drift recurrence (v2) |

#### Existing Columns Used/Extended

| Column | Notes |
|--------|------|
| `status` | Extended v2 statuses (see below). Legacy `acknowledged` is mapped to v2 `triaged` in the UI and migrated during backfill. |
| `resolved_at` / `resolved_reason` | Remains the terminal “resolved” record with reason. |
| `acknowledged_at` / `acknowledged_by_user_id` | Retained for historical reference; `acknowledged` status is legacy. |
| `fingerprint` | Remains unique per tenant. For canonical drift rows going forward, the fingerprint is stable (aligned to recurrence identity). |

#### Status Values (Canonical)

Open statuses:
- `new`
- `triaged`
- `in_progress`
- `reopened`

Terminal statuses:
- `resolved`
- `closed`
- `risk_accepted`

Legacy status (migration window):
- `acknowledged` (treated as `triaged` in v2 surfaces)

#### Indexes

New/updated indexes to support list filters and alert evaluation:

| Index | Type | Purpose |
|------|------|---------|
| `(tenant_id, status, due_at)` | btree | Open/overdue filtering in tenant UI |
| `(tenant_id, assignee_user_id)` | btree | “My assigned” filter |
| `(tenant_id, recurrence_key)` | btree | Drift recurrence lookups and consolidation |
| `(workspace_id, status, due_at)` | btree | Workspace-scoped SLA due producer query |

Existing index `(tenant_id, status)` remains valid.

---

## Configuration Keys (No Schema Change)

### Settings: Findings SLA policy

Add a SettingsRegistry entry (workspace-resolvable):
- `findings.sla_days` (JSON object): severity → days
- Default:
  - critical: 3
  - high: 7
  - medium: 14
  - low: 30

Stored in existing `workspace_settings` / `tenant_settings` tables; no new tables required.

---

## State Machine (High-Level)

```
new ──triage──> triaged ──start──> in_progress ──resolve──> resolved
  └───────────────────────────────close/risk_accept──────────────> closed|risk_accepted

resolved ──(auto or manual)──> reopened ──triage──> triaged ...
closed|risk_accepted ──(manual only)──> reopened
```

