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