# Implementation Plan: Drift Golden Master Cutover (Baseline Compare) **Branch**: `feat/119-baseline-drift-engine` | **Date**: 2026-03-05 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/spec.md` **Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/spec.md` ## Summary Make Baseline Compare the only drift findings writer (`source = baseline.compare`) while preserving the existing diff UI by writing a diff-compatible `evidence_jsonb` structure (including `summary.kind` and baseline/current `policy_version_id` references when content evidence exists). `different_version` findings require both refs, while `missing_policy` and `unexpected_policy` may render against an empty side when exactly one ref exists. Align Baseline Snapshot capture with the latest completed Inventory Sync run so stale inventory rows do not create false `ambiguous_match` gaps. Treat full-content compare captures that reuse an identical existing `policy_version` as valid current evidence for the current run, instead of dropping them behind the `since` filter. Treat Intune compliance `scheduledActionsForRule` as canonical drift signal content (semantic fields only, deterministically sorted) and keep existing baseline snapshots comparable by recomputing effective baseline hashes from resolved baseline `policy_versions` when content provenance exists. Remove the legacy run-to-run drift generator (jobs, UI, run-type catalog, alerts/widget references, and tests) and delete legacy drift findings to avoid mixed evidence formats. ## Technical Context **Language/Version**: PHP 8.4.1 **Primary Dependencies**: Laravel 12, Filament 5, Livewire 4 **Storage**: PostgreSQL (JSONB for evidence payloads) **Testing**: Pest 4 (PHPUnit 12) **Target Platform**: Linux containers (Dokploy) + local dev via Laravel Sail **Project Type**: Web application (Laravel + Filament admin panels) **Performance Goals**: Drift findings from a completed Baseline Compare run are visible within 5 minutes (spec SC-119-05); tenant pages render without timeouts under normal tenant sizes. **Constraints**: Hard cut (no feature flags, no dual-write), strict tenant/workspace isolation (404/403 semantics), Ops-UX `OperationRun` contract must remain intact, no new external integrations. **Scale/Scope**: Tenant-scoped drift findings + baseline compare runs; evidence enrichment must be deterministic and safe for list/detail rendering across typical governance datasets (paginated Findings list; evidence JSON schema remains stable). ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | Principle | Status | Notes | |---|---|---| | Inventory-first, Snapshots-second | PASS | Baseline Compare compares workspace-owned baseline snapshots against tenant “last observed” inventory/policy-version evidence; no change to snapshot immutability. | | Read/Write separation by default | PASS | Feature is drift detection + evidence enrichment; user-facing mutation remains Baseline Compare “Compare Now” (queued op with confirmation + audit). | | Single contract path to Graph | PASS | No new Graph endpoints; Baseline Compare continues to use existing evidence providers / contracts. | | Workspace + Tenant isolation (404/403 semantics) | PASS | Tenant-scoped Findings + Baseline Compare landing remain capability-gated; cleanup migration is scoped to drift findings only. | | Run observability + Ops-UX 3-surface feedback | PASS | Baseline Compare continues using `OperationRun`; removing legacy drift generation reduces competing run types and preserves Monitoring contract. | | Filament action surface contract | PASS | Drift entry point is Baseline Compare landing; Findings resource remains list+view with inspect affordance; destructive-like actions keep confirmations. | Gate decision: **PASS** (no constitution exceptions required). ## Project Structure ### Documentation (this feature) ```text specs/119-baseline-drift-engine/ ├── plan.md # This file (/speckit.plan command output) ├── research.md # Phase 0 output (/speckit.plan command) ├── data-model.md # Phase 1 output (/speckit.plan command) ├── quickstart.md # Phase 1 output (/speckit.plan command) ├── contracts/ # Phase 1 output (/speckit.plan command) └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) ``` ### Source Code (repository root) ```text /Users/ahmeddarrazi/Documents/projects/TenantAtlas/ ├── app/ │ ├── Filament/ │ │ ├── Pages/ │ │ │ ├── BaselineCompareLanding.php │ │ │ └── DriftLanding.php # legacy (to remove) │ │ ├── Resources/ │ │ │ └── FindingResource.php │ │ └── Widgets/ │ │ └── Dashboard/NeedsAttention.php # legacy references (to update) │ ├── Jobs/ │ │ ├── CompareBaselineToTenantJob.php │ │ └── GenerateDriftFindingsJob.php # legacy (to remove) │ ├── Jobs/Alerts/EvaluateAlertsJob.php # legacy compare_failed producer (to update/remove) │ ├── Services/ │ │ ├── Baselines/ │ │ │ ├── CurrentStateHashResolver.php │ │ │ └── Evidence/* # evidence providers + provenance │ │ └── Drift/* # keep diff + normalizers; remove generator-only pieces │ └── Support/ │ ├── OperationCatalog.php # legacy run type label (to remove) │ ├── OperationRunLinks.php # legacy drift link (to update) │ └── OperationRunType.php # legacy enum case (to remove) ├── database/migrations/ │ └── (new) *_delete_legacy_drift_findings.php # one-time cleanup migration ├── resources/views/filament/pages/ │ ├── baseline-compare-landing.blade.php │ └── drift-landing.blade.php # legacy (to remove) └── tests/ ├── Feature/Alerts/* # update references to legacy drift type if any └── Feature/Drift/* # legacy tests (to remove/update) ``` **Structure Decision**: Single Laravel application under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/` (Filament UI + queued jobs). Feature work stays within `app/`, `resources/`, `database/`, and `tests/` plus feature docs under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/`. ## Complexity Tracking > **Fill ONLY if Constitution Check has violations that must be justified** No constitution exceptions are required for this feature. ## Phase 0 — Outline & Research **Output**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/research.md` - Confirm existing Baseline Compare drift writer contract (where `source` is set; what evidence fields exist today). - Identify the minimum evidence keys required for the existing diff renderer (`summary.kind`, baseline/current `policy_version_id`). - Identify all legacy drift generation touchpoints to remove (job, generator, UI landing, dashboard widget, operation catalog/links, alerts producer, and tests). - Define deterministic cleanup criteria for legacy drift findings deletion. ## Phase 1 — Design & Contracts **Outputs**: - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/data-model.md` - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/contracts/*` - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/quickstart.md` Design highlights: - Drift findings remain tenant-owned (`findings.finding_type = drift`) with mandatory `source = baseline.compare`. - Baseline Compare evidence is enriched to be diff-renderable when content evidence exists: - Always write `evidence_jsonb.summary.kind` (allowed: `policy_snapshot`, `policy_assignments`, `policy_scope_tags`). - Write `evidence_jsonb.baseline.policy_version_id` and `evidence_jsonb.current.policy_version_id` when available (independently); compute `evidence_jsonb.fidelity` deterministically from presence (both = `content`, one = `mixed`, none = `meta`). - Resolve baseline `policy_version_id` deterministically using baseline snapshot item provenance (`observed_at`) + stable policy identity (`policy_type` + `subject_key`); if not resolvable, set null. - Add `evidence_jsonb.provenance` keys: `baseline_profile_id`, `baseline_snapshot_id`, `compare_operation_run_id` (Baseline Compare `OperationRun` id), and `inventory_sync_run_id` when applicable. - Findings UI renders one-sided diffs for `missing_policy` (baseline-only) and `unexpected_policy` (current-only); only `different_version` requires both refs. - Drift navigation entry point after cutover is Baseline Compare landing (`/admin/t/{tenant}/baseline-compare-landing`). Re-check Constitution post-design: **PASS** (design stays within tenant/workspace isolation and existing `OperationRun` + Filament action-surface rules). ## Phase 2 — Implementation Plan (for /speckit.tasks) Phase 2 will be expressed as concrete tasks in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/119-baseline-drift-engine/tasks.md` by `/speckit.tasks`. High-level sequencing: 1) **Evidence contract upgrade (Baseline Compare → drift findings)** - Extend Baseline Compare drift evidence writer to include `summary.kind`, baseline/current `policy_version_id` references (when content), explicit fidelity, and run/snapshot provenance. - Ensure evidence keys are stable and deterministic across runs. - Align Baseline Snapshot capture subject selection with the latest completed Inventory Sync run when available, matching Baseline Compare’s current-state boundary. - Treat same-run reused compare versions as valid current content evidence so deduplicated captures do not surface as `missing_current`. - Canonicalize compliance `scheduledActionsForRule` into semantic drift keys and compare content-backed baseline snapshots against recomputed baseline-version hashes so the signal can evolve without forcing snapshot recapture. 2) **Diff UX guardrails** - Update the Findings detail view so `different_version` diffs require both `baseline.policy_version_id` and `current.policy_version_id`, while `missing_policy` and `unexpected_policy` render against an empty side when their single required reference exists; otherwise show an explicit “diff unavailable” explanation. 3) **Hard-cut legacy drift removal** - Delete legacy drift generation job + generator-only services and remove tenant UI entry points that trigger or describe run-to-run drift generation. - Remove legacy operation run type registrations/catalog labels and any UI/widget/alert producers that reference drift generation. 4) **Data cleanup** - Add a one-time migration deleting findings where `finding_type = drift` AND (`source` is null OR `source` is not equal to `baseline.compare`), ensuring Baseline Compare rows remain. 5) **Tests** - Add/adjust tests asserting Baseline Compare drift findings include the new evidence contract and `source`. - Remove legacy drift generation tests and update any other tests that referenced `drift_generate_findings` only to simulate “some other type”.