# 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 ```