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

4.7 KiB
Raw Blame History

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').