# Research: Finding Risk Acceptance Lifecycle

## Decision 1: Use a dedicated tenant-owned exception aggregate instead of overloading `Finding.closed_reason`

**Decision**: Introduce a dedicated `FindingException` aggregate as the tenant-owned governance record for accepted risk, rather than continuing to encode risk acceptance purely as `Finding.status = risk_accepted` plus `closed_reason`.

**Rationale**: The existing finding model already exposes `risk_accepted` as a terminal status, but the handover and roadmap explicitly identify the absence of a formal exception entity as the product gap. A dedicated aggregate lets the system track request, approval, rejection, renewal, revocation, expiry, accountable owner, and linked evidence without distorting the meaning of generic finding workflow fields.

**Alternatives considered**:
- Reuse `Finding.closed_reason` and audit metadata only: rejected because it cannot represent a durable approval lifecycle or one current valid exception per finding.
- Create a generic cross-domain waiver engine immediately: rejected because the spec is intentionally bounded to finding-specific exceptions in the first rollout.

## Decision 2: Preserve history via append-only decision records under one exception root

**Decision**: Model exception history as one root `FindingException` record with append-only `FindingExceptionDecision` child records for request, approval, rejection, renewal, and revocation decisions.

**Rationale**: The product needs both a stable current-state record for efficient tenant and canonical queries and a durable history that survives renewals and revocations. A root aggregate with child decisions avoids rewriting old decisions, keeps list queries fast, and aligns with the repo's audit-first lifecycle design.

**Alternatives considered**:
- Create a new top-level exception row for every renewal: rejected because current-state lookup and canonical queue filtering become noisier and require additional dedupe logic.
- Store all history only in `AuditLog`: rejected because lifecycle state and validity queries would depend on replaying historical events instead of reading domain state.

## Decision 3: Block self-approval by default in v1

**Decision**: Normal workflow blocks the requester from approving their own exception request. No self-approval override is included in the first slice.

**Rationale**: The spec requires approval-separation rules, and a default no-self-approval rule is the clearest governance baseline. It reduces ambiguity, simplifies policy design, and matches the product's least-privilege posture.

**Alternatives considered**:
- Allow self-approval for owners or managers: rejected because it weakens the governance signal and creates policy ambiguity in the first rollout.
- Introduce a special override capability immediately: rejected because it expands RBAC and exception policy complexity before the core workflow is proven.

## Decision 4: Keep normal exception decisions outside `OperationRun`

**Decision**: Request, approval, rejection, renewal, and revocation remain synchronous DB-backed mutations without a dedicated `OperationRun` in v1.

**Rationale**: These actions are local governance decisions, expected to complete quickly, and do not perform remote work. The constitution allows DB-only security-relevant actions to skip `OperationRun` as long as they remain auditable. Using `OperationRun` here would add operational surface area without adding observability value.

**Alternatives considered**:
- Use `OperationRun` for every exception decision: rejected because it violates the repo's preference to avoid long-running infrastructure for fast DB-only mutations.
- Add a scheduled reminder/expiry job in the first slice: rejected because the first release can satisfy reminder semantics through explicit expiring-state UI and canonical queue visibility.

## Decision 5: Link supporting evidence through structured references, not copied payloads

**Decision**: Exception records store structured evidence references such as `source_type`, `source_id`, `source_fingerprint`, and a small summary snapshot, following the evidence snapshot item pattern instead of embedding raw evidence payloads.

**Rationale**: The repo already uses fingerprinted and summarized evidence references in `EvidenceSnapshotItem` and review-pack generation. Reusing that pattern keeps exception history intelligible even when live artifacts change, while preserving data minimization.

**Alternatives considered**:
- Store raw evidence JSON directly on the exception: rejected because it increases payload size and risks leaking data better handled by the evidence domain.
- Store only foreign keys to live evidence records: rejected because history becomes opaque if referenced artifacts are later expired or superseded.

## Decision 6: Risk governance validity is derived from exception state, not from finding status alone

**Decision**: A finding counts as currently valid accepted risk only when it is linked to an active, unexpired, unrevoked exception. The finding's `risk_accepted` status alone is insufficient.

**Rationale**: This closes the core audit gap identified in the handover and allows evidence and reporting consumers to distinguish governed accepted risk from stale or unsupported states. The existing `FindingWorkflowService` remains the single mutation path for the finding status, but validity becomes a cross-record rule.

**Alternatives considered**:
- Treat `Finding.status = risk_accepted` as sufficient forever: rejected because it preserves the current governance gap.
- Automatically revert finding status when an exception expires: rejected because it mutates the finding lifecycle as a side effect and obscures historical operator intent.