TenantAtlas/specs/119-baseline-drift-engine/data-model.md
ahmido da1adbdeb5 Spec 119: Drift cutover to Baseline Compare (golden master) (#144)
Implements Spec 119 (Drift Golden Master Cutover):

- Baseline Compare is the only drift writer (`source = baseline.compare`).
- Drift findings now store diff-compatible `evidence_jsonb` (summary.kind, baseline/current policy_version_id refs, fidelity + provenance).
- Findings UI renders one-sided diffs for `missing_policy`/`unexpected_policy` when a single ref exists; otherwise shows explicit “diff unavailable”.
- Removes legacy drift generator runtime (jobs/services/UI) and related tests.
- Adds one-time migration to delete legacy drift findings (`finding_type=drift` where source is null or != baseline.compare).
- Scopes baseline capture & landing duplicate warnings to latest completed inventory sync.
- Canonicalizes compliance `scheduledActionsForRule` drift signal and keeps legacy snapshots comparable.

Tests:
- `vendor/bin/sail artisan test --compact` (full suite per tasks)
- Focused pack: BaselinePolicyVersionResolverTest, BaselineCompareDriftEvidenceContractTest, DriftFindingDiffUnavailableTest, LegacyDriftFindingsCleanupMigrationTest, ComplianceNoncomplianceActionsDriftTest

Notes:
- Livewire v4+ / Filament v5 compatible (no legacy APIs).
- No new external dependencies.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #144
2026-03-06 14:30:49 +00:00

89 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Data Model — Drift Golden Master Cutover (Spec 119)
This spec extends existing models and introduces no new tables.
## Entities
### 1) Finding (existing: `App\Models\Finding`)
Baseline Compare drift findings are tenant-owned rows in `findings`.
Key fields used/extended by this feature:
- `workspace_id` (derived from tenant; required)
- `tenant_id` (required)
- `finding_type = drift` (required)
- `source = baseline.compare` (mandatory contract post-cutover)
- `scope_key` (baseline compare grouping key; stable across re-runs)
- `fingerprint` + `recurrence_key` (stable identity for idempotent upsert and lifecycle)
- `severity` (`low|medium|high|critical`)
- `status` (lifecycle): `new`, `reopened`, other open states, terminal states
- `evidence_fidelity` (string): `content|meta|mixed`
- `evidence_jsonb` (JSONB): enriched evidence contract (see below)
- `current_operation_run_id` (the Baseline Compare `OperationRun` that observed the finding)
- `baseline_operation_run_id` (unused for Baseline Compare drift; legacy run-to-run drift used it)
#### Evidence contract (`findings.evidence_jsonb`)
Minimum required keys for diff-UX compatibility:
- `change_type` (string): `missing_policy|unexpected_policy|different_version`
- `policy_type` (string)
- `subject_key` (string)
- `summary.kind` (string): `policy_snapshot|policy_assignments|policy_scope_tags`
- `baseline.policy_version_id` (int|null)
- `current.policy_version_id` (int|null)
- `baseline.hash` / `current.hash` (string; may be empty for missing/unexpected)
- `baseline.provenance` / `current.provenance` (object; fidelity/source/observed_at/observed_operation_run_id)
- `fidelity` (string): `content|meta|mixed`
- `provenance` (object): `baseline_profile_id`, `baseline_snapshot_id`, `compare_operation_run_id`, and `inventory_sync_run_id` when applicable
Invariant:
- `different_version` requires both policy version ids to render a detailed diff.
- `missing_policy` may render a detailed diff with only `baseline.policy_version_id`.
- `unexpected_policy` may render a detailed diff with only `current.policy_version_id`.
- If the required policy version reference(s) for the findings change type are missing, the UI must treat the finding as “diff unavailable”.
### 2) OperationRun (existing: `App\Models\OperationRun`)
Baseline Compare runs:
- `type = baseline_compare`
- `tenant_id` is required (tenant-scoped operation)
- `status/outcome` transitions are service-owned (must go through `OperationRunService`)
- `context.baseline_profile_id` and `context.baseline_snapshot_id` identify the compare baseline inputs
- `context.baseline_compare.*` contains coverage + evidence gap breakdowns (read-only reporting)
Baseline Capture runs:
- `type = baseline_capture`
- `context.baseline_profile_id` and `context.source_tenant_id` identify the capture inputs
- `context.baseline_capture.inventory_sync_run_id` records which completed Inventory Sync bounded subject selection when one existed
- `context.baseline_capture.gaps.*` remains the canonical reporting block for snapshot-capture ambiguity or evidence issues
Legacy run-to-run drift generation runs:
- `type = drift_generate_findings` (no longer created post-cutover; existing rows may remain historical)
### 3) BaselineProfile / BaselineSnapshot / BaselineSnapshotItem (existing)
Workspace-owned baseline objects:
- Baseline Profile (`baseline_profiles`) defines scope + capture mode and points to `active_snapshot_id`.
- Baseline Snapshot (`baseline_snapshots`) is immutable and stores `summary_jsonb`.
- Baseline Snapshot Items (`baseline_snapshot_items`) store:
- `baseline_hash` (string)
- `meta_jsonb.evidence` provenance fields (fidelity/source/observed_at), intentionally without tenant-owned identifiers (e.g., no `policy_version_id`).
### 4) PolicyVersion (existing: `App\Models\PolicyVersion`)
Tenant-owned immutable policy snapshots used for:
- Content hashing/normalization for Baseline Compare evidence
- Rendering diffs in Findings detail view when both baseline/current policy version references exist
Relevant fields:
- `id`
- `tenant_id`
- `policy_id` (ties versions to a tenant policy)
- `capture_purpose` (e.g., `baseline_capture`, `baseline_compare`)
- `operation_run_id` (which run captured it)
- `captured_at`
- `snapshot`, `assignments`, `scope_tags` (JSON/arrays)
## Derived/Computed Values
- `evidence_jsonb.fidelity` should align with (or be derived from) the per-finding `evidence_fidelity` column.
- `summary.kind` may be set conservatively (e.g., `policy_snapshot`) when dimension-level detection is not available.
## Invariants
- Post-cutover drift findings must be queryable as: `finding_type = drift AND source = baseline.compare`.
- Legacy drift findings are deleted by a one-time migration using: `finding_type = drift AND (source IS NULL OR source <> 'baseline.compare')`.