TenantAtlas/specs/119-baseline-drift-engine/research.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

9.7 KiB
Raw Permalink Blame History

Research — Drift Golden Master Cutover (Spec 119)

This document resolves planning unknowns and records implementation decisions for making Baseline Compare the single source of truth for drift findings while preserving the existing diff UI.

Decisions

1) Golden-master drift source

  • Decision: All drift findings generated by Baseline Compare will use findings.source = baseline.compare.
  • Rationale: This is the single “origin label” used across the spec and is already set in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php when upserting findings.
  • Alternatives considered:
    • Keep source nullable / optional → rejected because it enables mixed states and breaks the single-source contract.

2) Drift navigation entry point (post-cutover)

  • Decision: The Drift navigation entry point becomes the Baseline Compare landing page (/admin/t/{tenant}/baseline-compare-landing).
  • Rationale: This preserves a single operational entry point for drift generation and reduces duplicated UI “landing” surfaces.
  • Alternatives considered:
    • Keep a separate Drift landing page and repurpose it → rejected (extra surface to maintain and re-explain).

3) Evidence contract for diff UX compatibility

  • Decision: Baseline Compare drift findings will write evidence_jsonb keys required by the existing diff renderer:
    • summary.kind with allowed values: policy_snapshot, policy_assignments, policy_scope_tags
    • baseline.policy_version_id and current.policy_version_id when content evidence exists
    • Explicit fidelity labeling + explicit compare provenance (baseline profile/snapshot + compare run id + inventory sync run id when available)
  • Rationale: /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php uses summary.kind to decide which diff UI to render and reads the policy version IDs from baseline.policy_version_id and current.policy_version_id.
  • Alternatives considered:
    • Introduce a new diff UI for Baseline Compare evidence → rejected (scope; requires new UI + new contract).

4) Diff renderability rule (avoid misleading empty diffs)

  • Decision: Only render a detailed diff when both baseline.policy_version_id and current.policy_version_id are present; otherwise show “diff unavailable”.
  • Rationale: The diff builder can otherwise compare empty/null versions and display misleading results; the spec requires an explicit “diff unavailable” explanation.
  • Alternatives considered:
    • Render diffs even when one side is missing → rejected (misleading output; violates clarified rule).

8) One-sided diff rendering for policy presence changes

  • Decision: Render diffs against an empty side for missing_policy (baseline-only reference) and unexpected_policy (current-only reference). Keep the stricter two-reference rule only for different_version.
  • Rationale: Policy presence changes are easier to understand when operators can inspect the captured policy content that exists, instead of receiving a generic “diff unavailable” message.
  • Alternatives considered:
    • Keep treating all single-reference findings as non-renderable → rejected (hides useful evidence even when one side is fully captured).

9) Baseline capture must ignore stale inventory rows

  • Decision: When a latest completed Inventory Sync exists, Baseline Snapshot capture scopes inventory_items to that run before deriving subject_key matches.
  • Rationale: Capture and Compare must agree on the same “current observed state” boundary; otherwise deleted/renamed policies from older syncs can create false ambiguous_match gaps and omit valid baseline subjects.
  • Alternatives considered:
    • Continue scanning all tenant inventory rows during capture → rejected (nondeterministic snapshot gaps as historical rows accumulate).
    • Hard-fail capture when no completed Inventory Sync exists → deferred (larger product behavior change than this fix; current fallback remains acceptable).

10) Full-content compare must reuse same-run deduplicated evidence

  • Decision: When the compare-time content capture fetches current policy content successfully but reuses an older identical policy_version row instead of inserting a new one, the compare run will consume that returned version directly as current evidence for the run.
  • Rationale: The capture step has already validated current Graph content. Re-querying only by captured_at >= snapshot.captured_at misclassifies these successful deduplicated captures as missing_current, which incorrectly downgrades fidelity and emits evidence_capture_incomplete.
  • Alternatives considered:
    • Always insert a new policy_version row per compare run → rejected (breaks immutable dedupe strategy and inflates storage).
    • Keep relying only on the post-capture since query → rejected (produces false partial-success outcomes when content is unchanged).

11) Landing-page duplicate warnings must use the latest sync boundary

  • Decision: The Baseline Compare landing-page duplicate-name warning uses the latest completed Inventory Sync run when one exists, matching compare/capture subject selection.
  • Rationale: Operators should not keep seeing a duplicate-name warning after the duplicate only survives in stale historical inventory rows; the landing page must reflect the same current boundary as the underlying compare logic.
  • Alternatives considered:
    • Keep scanning all tenant inventory rows for the warning → rejected (UI keeps reporting already-resolved duplicates until stale rows are cleaned up out-of-band).

12) Compliance noncompliance actions belong in the policy drift signal

  • Decision: deviceCompliancePolicy.scheduledActionsForRule participates in policy_snapshot drift through a canonical semantic projection of each configured action.
  • Rationale: A compliance policys security effect depends on both the rule and its enforcement timeline/consequences. Changing gracePeriodHours, removing retire, or swapping notification templates changes governance behavior and must produce drift.
  • Alternatives considered:
    • Ignore noncompliance actions entirely → rejected (false negatives on meaningful governance changes).
    • Hash the raw Graph array directly → rejected (opaque IDs and order churn would create false positives).

13) Expand the drift signal without forcing baseline recapture

  • Decision: When baseline content provenance resolves to a tenant policy_version, Compare recomputes the effective baseline content hash from that immutable version instead of trusting only the stored snapshot hash.
  • Rationale: Existing baseline snapshots were captured under older normalization semantics. Recomputing from the resolved baseline version keeps those snapshots comparable as the canonical drift signal expands, which avoids rollout-time false positives and avoids forcing operators to recapture unchanged baselines.
  • Alternatives considered:
    • Require every tenant to recapture their baseline after signal changes → rejected (operationally brittle and easy to miss).
    • Keep comparing only the stored snapshot hash → rejected (old snapshots would flap as soon as the drift signal grows).

5) How policy version references are populated

  • Decision:
    • Current-side policy_version_id: taken from content evidence (ResolvedEvidence.meta.policy_version_id) when content fidelity is used.
    • Baseline-side policy_version_id: resolved opportunistically for the same tenant policy when baseline-side evidence is content-based (e.g., via baseline-capture policy versions), otherwise set to null.
  • Rationale: Baseline snapshots are workspace-owned and intentionally avoid persisting tenant-owned identifiers; the finding (tenant-owned) is the correct place to attach tenant-specific policy version references.
  • Alternatives considered:
    • Persist baseline policy version IDs in baseline snapshots → rejected (violates scope/ownership model for workspace-owned snapshots).

6) Legacy drift findings deletion criteria

  • Decision: One-time cleanup deletes drift findings where source is null or not equal to baseline.compare (scoped to finding_type = drift), and keeps source = baseline.compare rows.
  • Rationale: Legacy drift generator rows often have source = NULL; this filter removes mixed evidence formats without risking Baseline Compare drift data.
  • Alternatives considered:
    • Delete by “old evidence shape” heuristics only → rejected (brittle; source is the canonical differentiator post-cutover).

7) Legacy drift generator removal scope

  • Decision: Remove legacy run-to-run drift generation end-to-end:
    • GenerateDriftFindingsJob + generator-only services
    • Drift landing UI surface that triggers legacy drift generation
    • Operation run type catalog entries and any related UI/widget/alert producer references
    • Legacy tests that assert drift generation dispatch/notifications
  • Rationale: Hard cut means no dual-write/no feature flags; leaving legacy entry points risks reintroducing “two truths”.
  • Alternatives considered:
    • Leave legacy components present but unreachable → rejected (dead code + drift risk).

Notes / Repo Facts Used

  • Baseline Compare upserts findings and already hard-sets source = baseline.compare in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Jobs/CompareBaselineToTenantJob.php.
  • The existing diff UI reads:
    • evidence_jsonb.summary.kind
    • evidence_jsonb.baseline.policy_version_id
    • evidence_jsonb.current.policy_version_id in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php.
  • Content evidence already carries policy_version_id in ResolvedEvidence.meta via /Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Services/Baselines/Evidence/ContentEvidenceProvider.php.