# Data Model: Finding Risk Acceptance Lifecycle

## 1. FindingException

- **Purpose**: Tenant-owned governance aggregate that represents the current accepted-risk exception state for one finding.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
  - `id`
  - `workspace_id`
  - `tenant_id`
  - `finding_id`
  - `status` enum: `pending`, `active`, `expiring`, `expired`, `rejected`, `revoked`, `superseded`
  - `requested_by_user_id`
  - `owner_user_id`
  - `approved_by_user_id` nullable
  - `current_decision_id` nullable
  - `request_reason` text
  - `approval_reason` text nullable
  - `rejection_reason` text nullable
  - `revocation_reason` text nullable
  - `requested_at`
  - `approved_at` nullable
  - `rejected_at` nullable
  - `revoked_at` nullable
  - `effective_from` nullable
  - `expires_at` nullable
  - `review_due_at` nullable
  - `evidence_summary` JSONB nullable
  - `current_validity_state` enum: `valid`, `expiring`, `expired`, `revoked`, `rejected`, `missing_support`
  - `created_at`, `updated_at`
- **Relationships**:
  - belongs to `Finding`
  - belongs to `Tenant`
  - belongs to `Workspace`
  - belongs to requester `User`
  - belongs to owner `User`
  - belongs to approver `User`
  - has many `FindingExceptionDecision`
  - has many `FindingExceptionEvidenceReference`
- **Validation / invariants**:
  - `workspace_id`, `tenant_id`, and `finding_id` are always required.
  - `finding_id` must reference a finding in the same workspace and tenant.
  - At most one current valid active exception may govern one finding at a time.
  - `approved_by_user_id` must differ from `requested_by_user_id` in v1.
  - `expires_at` must be after `effective_from` when both are present.

## 2. FindingExceptionDecision

- **Purpose**: Append-only historical record of every exception lifecycle decision.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
  - `id`
  - `workspace_id`
  - `tenant_id`
  - `finding_exception_id`
  - `decision_type` enum: `requested`, `approved`, `rejected`, `renewal_requested`, `renewed`, `revoked`
  - `actor_user_id`
  - `reason` text nullable
  - `effective_from` nullable
  - `expires_at` nullable
  - `metadata` JSONB nullable
  - `decided_at`
  - `created_at`, `updated_at`
- **Relationships**:
  - belongs to `FindingException`
  - belongs to actor `User`
- **Validation / invariants**:
  - Decision rows are append-only after creation.
  - Decision type must be compatible with the parent exception's lifecycle state.
  - Renewal decisions must not erase prior approval or rejection records.

## 3. FindingExceptionEvidenceReference

- **Purpose**: Structured pointer to evidence used to justify or review the exception.
- **Ownership**: Tenant-owned (`workspace_id` + `tenant_id` NOT NULL).
- **Fields**:
  - `id`
  - `workspace_id`
  - `tenant_id`
  - `finding_exception_id`
  - `source_type` string
  - `source_id` string nullable
  - `source_fingerprint` string nullable
  - `label` string
  - `summary_payload` JSONB nullable
  - `measured_at` nullable
  - `created_at`, `updated_at`
- **Relationships**:
  - belongs to `FindingException`
- **Validation / invariants**:
  - References must stay intelligible even if the live source artifact later expires or is removed from active views.
  - `summary_payload` is bounded, sanitized, and not a raw payload dump.

## 4. Finding Risk Governance Projection

- **Purpose**: Derived truth used by finding detail, tenant exception lists, canonical queues, and downstream evidence/reporting consumers.
- **Derived from**:
  - `Finding.status`
  - `FindingException.status`
  - exception validity window (`effective_from`, `expires_at`)
  - current exception evidence support state
- **Values**:
  - `ungoverned`
  - `pending_exception`
  - `valid_exception`
  - `expiring_exception`
  - `expired_exception`
  - `revoked_exception`
  - `rejected_exception`
  - `risk_accepted_without_valid_exception`
- **Invariant**:
  - Downstream consumers must use this projection, not finding status alone, when determining whether accepted risk is currently governed.

## State Transitions

### FindingException

- `pending` -> `active` on approval
- `pending` -> `rejected` on rejection
- `active` -> `expiring` when within reminder threshold
- `active|expiring` -> `expired` when `expires_at` passes
- `active|expiring` -> `revoked` on explicit revoke
- `active|expiring|expired` -> `superseded` when a renewal produces a newer governing decision under the same aggregate semantics

### FindingExceptionDecision

- `requested` always occurs first
- `approved` or `rejected` resolves a pending request
- `renewal_requested` may occur from `active`, `expiring`, or `expired`
- `renewed` extends a current or lapsed exception through a new decision
- `revoked` ends current validity explicitly

## Indexing and Query Needs

- Composite indexes on `(workspace_id, tenant_id, status)` for tenant register filtering.
- Composite indexes on `(workspace_id, status, review_due_at)` for canonical queue and expiring views.
- Unique partial index to prevent more than one current valid active exception per finding.
- Composite index on `(finding_id, tenant_id)` for finding-detail resolution.

## Relationship to Existing Domain Records

- `Finding` remains the system of record for the detected issue and status workflow.
- `FindingWorkflowService` remains the only allowed path for changing finding status.
- `AuditLog` remains the immutable historical event stream for every lifecycle mutation.
- `EvidenceSnapshot` and related artifacts remain separate systems of record referenced by exception evidence links.