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