# Data Model — Enforce Creation-Time Finding Invariants

**Spec**: [spec.md](spec.md)

This feature introduces no new persisted truth. The data-model impact is to make the existing `Finding` lifecycle contract explicit at create, refresh, and reopen time across the three active writer families.

## Existing Canonical Entities Reused

### Finding (`findings`)

**Purpose**: Tenant-owned findings workflow truth.

**Key fields already in use**:
- `id`
- `workspace_id`
- `tenant_id`
- `finding_type`
- `source`
- `scope_key`
- `fingerprint`
- `recurrence_key`
- `severity`
- `status`
- `first_seen_at`
- `last_seen_at`
- `times_seen`
- `sla_days`
- `due_at`
- `reopened_at`
- `resolved_at`
- `resolved_reason`
- `closed_at`
- `closed_reason`
- `current_operation_run_id`
- `baseline_operation_run_id`

**Feature use**:
- Remains the single persisted source of truth for active findings lifecycle state.
- Continues to require both `workspace_id` and `tenant_id` anchors.
- Keeps the current status families unchanged.
- Carries the lifecycle-ready fields that this feature hardens at write time.

### OperationRun (`operation_runs`)

**Purpose**: Existing execution context for baseline compare and other operational flows.

**Feature use**:
- Remains contextual only.
- `current_operation_run_id` continues to identify the current writer run where the family already sets it.
- No new operation type or new run-tracking artifact is introduced.

### StoredReport (`stored_reports`)

**Purpose**: Existing stored reporting artifact for permission posture output.

**Feature use**:
- Unchanged.
- Mentioned only because permission posture finding generation already correlates lifecycle-ready findings with an existing report artifact.

## Derived Non-Persisted Contracts

### LifecycleReadyFinding (derived contract)

**Definition**: A `Finding` record that is immediately usable by the existing workflow the moment the active writer persists or refreshes it.

**Required fields**:
- active canonical status on first create (`new`)
- `first_seen_at`
- `last_seen_at`
- `times_seen >= 1`
- `sla_days` when the current severity policy returns a value
- `due_at` when the current severity policy requires due-date truth
- existing run correlation fields preserved where the writer already populates them

**Removal rule**:
- no later repair surface may be required for these fields on active writers

### RecurrenceIdentity (derived contract)

**Definition**: The family-owned identity that decides whether a repeated observation refreshes one canonical finding or incorrectly creates a duplicate.

**Family-specific variants**:
- baseline compare: `recurrence_key` and `fingerprint` derived from tenant, baseline profile, policy type, subject key, and change type
- Entra admin roles: existing role-assignment and aggregate fingerprints
- permission posture: existing permission and error fingerprints

**Guarantee**:
- repeated observation of the same canonical issue reuses one finding identity

### ObservationBoundary (derived contract)

**Definition**: The family-specific rule that decides whether `times_seen` should advance.

**Family-specific variants**:
- baseline compare: same `current_operation_run_id` must not increment `times_seen` twice for the same observation
- Entra admin roles: later `observedAt` advances seen history
- permission posture: later `observedAt` advances seen history

**Guarantee**:
- retries and repeated processing do not double count the same observation

## State Transitions Reused

### Create

- missing canonical finding identity -> create one `Finding`
- resulting state remains `new`
- lifecycle-ready fields are populated in the same write path

### Refresh Existing Open Finding

- existing open finding remains in its current active workflow state
- evidence or severity may refresh according to the writer family
- missing lifecycle-ready fields covered by this feature are repaired inline
- valid existing lifecycle fields should not be needlessly reset

### Reopen Existing Terminal Finding

- existing terminal finding transitions through `FindingWorkflowService::reopenBySystem()`
- resulting state becomes `reopened`
- `resolved_*` and `closed_*` markers clear according to the current service behavior
- SLA and due-state truth are recalculated from the later re-observation moment

## Invariant Rules

- No new persisted entity, table, or compatibility artifact may be introduced.
- No new workflow status, reopen reason family, or lifecycle label may be introduced.
- Active writers must repair incomplete lifecycle-ready fields inline rather than relying on CLI repair commands, tenant maintenance actions, or deploy-time hooks.
- Due-state repair should fill missing truth or refresh terminal-to-reopened truth only; it must not silently redesign current due-date semantics for already-healthy open findings.
- A later database constraint is a separate follow-up candidate only if application-level write-path enforcement proves insufficient.