# Data Model: Finding Outcome Taxonomy & Verification Semantics

## Overview

This feature does not add a new table or a new top-level persisted entity. It reuses the current `findings` row as the source of truth for current terminal meaning, keeps reopen rationale in audit metadata, and derives verification or reporting buckets through one findings-local semantics helper.

## Entity: Finding

**Persistence**: existing `findings` table  
**Owner**: tenant-owned record  
**Primary responsibility**: current workflow status, current terminal-outcome key, timestamps, and tenant-scoped operational ownership

### Relevant persisted fields

| Field | Type | Source | Notes |
|------|------|--------|-------|
| `id` | integer | existing | Primary key |
| `workspace_id` | integer | existing | Workspace isolation boundary |
| `tenant_id` | integer | existing | Tenant isolation boundary |
| `status` | string | existing | Primary lifecycle status; remains one of `new`, `acknowledged`, `triaged`, `in_progress`, `reopened`, `resolved`, `closed`, `risk_accepted` |
| `severity` | string | existing | Existing priority and SLA signal |
| `resolved_reason` | string nullable | existing | Becomes a bounded canonical resolve key instead of free-form prose |
| `closed_reason` | string nullable | existing | Becomes a bounded canonical close key and remains the stored reason for `risk_accepted` |
| `resolved_at` | datetime nullable | existing | Marks the current resolved terminal timestamp |
| `closed_at` | datetime nullable | existing | Marks the current closed or risk-accepted terminal timestamp |
| `reopened_at` | datetime nullable | existing | Marks the current reopen timestamp |
| `closed_by_user_id` | integer nullable | existing | Current actor for close and risk-accept paths |
| `owner_user_id` | integer nullable | existing | Accountability owner |
| `assignee_user_id` | integer nullable | existing | Active assignee |
| `evidence_jsonb` | jsonb | existing | Current supporting evidence; unchanged in this slice |

### Relationships

| Relationship | Type | Notes |
|-------------|------|-------|
| `tenant()` | belongsTo | Tenant scope owner |
| `ownerUser()` | belongsTo | Accountability owner |
| `assigneeUser()` | belongsTo | Active assignee |
| `closedByUser()` | belongsTo | Actor for close/risk accept |
| `findingException()` | hasOne | Existing risk-governance truth from Spec 154 |

### Validation rules introduced by this feature

| Rule | Description |
|------|-------------|
| `status` | No new status is introduced; all existing transition rules stay in force |
| `resolved_reason` | Required for resolve and system-clear transitions; must be a canonical key from the resolve-reason family |
| `closed_reason` | Required for close and risk-accept transitions; must be a canonical key from the close-reason family or the risk-accept key |
| `reopen_reason` | Required for reopen transitions; remains audit metadata and must be a canonical key from the reopen-reason family |

### Canonical reason families

#### Resolve reason keys

| Key | Meaning | Derived verification state |
|-----|---------|----------------------------|
| `remediated` | Operator declares that remediation work was completed | `pending_verification` |
| `no_longer_drifting` | Trusted compare no longer reproduces prior drift | `verified_cleared` |
| `permission_granted` | Trusted permission evidence no longer reproduces the finding condition | `verified_cleared` |
| `permission_removed_from_registry` | Trusted permission evidence confirms the triggering permission was removed | `verified_cleared` |
| `role_assignment_removed` | Trusted role evidence confirms the triggering assignment was removed | `verified_cleared` |
| `ga_count_within_threshold` | Trusted role evidence confirms the triggering count is now safe | `verified_cleared` |

#### Close reason keys

| Key | Meaning | Outcome family |
|-----|---------|----------------|
| `false_positive` | The finding should not have been actionable | `administrative_closure` |
| `duplicate` | The finding duplicates another case | `administrative_closure` |
| `no_longer_applicable` | The finding no longer applies to the tenant context | `administrative_closure` |
| `accepted_risk` | Existing accepted-risk path only; still governed by exception validity | `accepted_risk` |

#### Reopen reason keys

| Key | Meaning | Persistence |
|-----|---------|-------------|
| `recurred_after_resolution` | A previously addressed condition reappeared | audit metadata only |
| `verification_failed` | Trusted evidence contradicted the earlier resolved outcome | audit metadata only |
| `manual_reassessment` | An operator reopened the finding after review | audit metadata only |

### Derived facets from the current row

| Derived facet | Source | Meaning |
|--------------|--------|---------|
| `verification_state` | `status` + `resolved_reason` | `pending_verification`, `verified_cleared`, or `not_applicable` |
| `terminal_outcome_key` | `status` + canonical reason key | Stable UI/reporting key such as `resolved_pending_verification`, `verified_cleared`, `closed_false_positive`, `closed_duplicate`, `closed_no_longer_applicable`, or `risk_accepted` |
| `report_bucket` | `terminal_outcome_key` + governance validity | Report-friendly aggregation bucket |
| `outcome_label` | `terminal_outcome_key` | Canonical operator wording |

### State transitions

| From | Action | Stored result | Derived outcome |
|------|--------|---------------|-----------------|
| `new`, `triaged`, `in_progress`, `reopened`, `acknowledged` | `resolve(remediated)` | `status=resolved`, `resolved_reason=remediated` | `Resolved pending verification` |
| open statuses | `close(false_positive)` | `status=closed`, `closed_reason=false_positive` | `Closed as false positive` |
| open statuses | `close(duplicate)` | `status=closed`, `closed_reason=duplicate` | `Closed as duplicate` |
| open statuses | `close(no_longer_applicable)` | `status=closed`, `closed_reason=no_longer_applicable` | `Closed as no longer applicable` |
| open statuses | `riskAccept(accepted_risk)` | `status=risk_accepted`, `closed_reason=accepted_risk` | `Risk accepted` with separate governance validity |
| `resolved` with `resolved_reason=remediated` | trusted system clear | `status=resolved`, `resolved_reason=<system clear key>` | `Verified cleared` |
| open statuses | direct trusted system clear | `status=resolved`, `resolved_reason=<system clear key>` | `Verified cleared` |
| `resolved`, `closed`, `risk_accepted` | `reopen(<canonical reopen key>)` | `status=reopened`, terminal reason fields cleared, reopen reason recorded in audit metadata | open finding again |

## Entity: FindingException

**Persistence**: existing `finding_exceptions` table  
**Owner**: tenant-owned record  
**Primary responsibility**: governance validity for `risk_accepted`

### Relevant fields consumed by this feature

| Field | Type | Notes |
|------|------|-------|
| `status` | string | Existing workflow for exception requests and approvals |
| `current_validity_state` | string | Current validity used by `FindingRiskGovernanceResolver` |
| `effective_from` | datetime nullable | Existing validity window |
| `expires_at` | datetime nullable | Existing validity window |
| `review_due_at` | datetime nullable | Existing governance follow-up signal |

### Rule in this feature

- `FindingException` continues to determine whether `risk_accepted` is governed safely.
- `FindingException` does not contribute to `verified_cleared` and does not change remediation buckets.

## Derived read model: FindingOutcomeSemantics

**Persistence**: not persisted  
**Owner**: findings-local support helper  
**Primary responsibility**: unify list, detail, filter, and reporting semantics from current finding truth

### Inputs

| Input | Source |
|------|--------|
| `status` | `Finding` row |
| `resolved_reason` | `Finding` row |
| `closed_reason` | `Finding` row |
| `findingException` validity | `FindingException` relationship via existing resolver |
| `system_origin` and prior workflow steps | audit metadata, only when reconstructing history or detailed provenance |

### Outputs

| Output | Type | Notes |
|-------|------|-------|
| `terminalOutcomeKey` | string | Stable internal key |
| `label` | string | Canonical operator-facing wording |
| `verificationState` | string | `pending_verification`, `verified_cleared`, `not_applicable` |
| `reportBucket` | string | Aggregation bucket for reviews and exports |
| `historicalContext` | string nullable | Reuses current resolver-style explanatory text where appropriate |

### Report bucket mapping

| Terminal outcome key | Report bucket |
|----------------------|---------------|
| `resolved_pending_verification` | `remediation_pending_verification` |
| `verified_cleared` | `remediation_verified` |
| `closed_false_positive` | `administrative_closure` |
| `closed_duplicate` | `administrative_closure` |
| `closed_no_longer_applicable` | `administrative_closure` |
| `risk_accepted` | `accepted_risk` |

## Audit metadata additions or constraints

| Key | Existing/New | Purpose |
|-----|--------------|---------|
| `resolved_reason` | existing | Canonical current resolve key |
| `closed_reason` | existing | Canonical current close or risk-accept key |
| `reopened_reason` | existing | Canonical reopen key for reviewability |
| `system_origin` | existing | Provenance flag for system transitions |
| `resolved_at`, `closed_at`, `reopened_at` | existing | Timeline reconstruction |

### Audit rule

- Audit metadata remains the place to reconstruct path history, such as whether a verified-clear outcome happened directly through automation or after a prior manual `remediated` transition.
- The current row remains the source of truth for current filters and summaries.