Implements Spec 116 baseline drift engine v1 (meta fidelity) with coverage guard, stable finding identity, and Filament UI surfaces. Highlights - Baseline capture/compare jobs and supporting services (meta contract hashing via InventoryMetaContract + DriftHasher) - Coverage proof parsing + compare partial outcome behavior - Filament pages/resources/widgets for baseline compare + drift landing improvements - Pest tests for capture/compare/coverage guard and UI start surfaces - Research report: docs/research/golden-master-baseline-drift-deep-analysis.md Validation - `vendor/bin/sail bin pint --dirty` - `vendor/bin/sail artisan test --compact --filter="Baseline"` Notes - No destructive user actions added; compare/capture remain queued jobs. - Provider registration unchanged (Laravel 11+/12 uses bootstrap/providers.php for panel providers; not touched here). Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #141
6.4 KiB
Phase 0 — Research (Baseline Drift Engine)
This document resolves the open design/implementation questions needed to produce a concrete implementation plan for Spec 116, grounded in the current codebase.
Repo Reality Check (What already exists)
- Baseline domain tables exist:
baseline_profiles,baseline_snapshots,baseline_snapshot_items,baseline_tenant_assignments. - Baseline ops exist:
- Capture:
App\Services\Baselines\BaselineCaptureService→App\Jobs\CaptureBaselineSnapshotJob. - Compare:
App\Services\Baselines\BaselineCompareService→App\Jobs\CompareBaselineToTenantJob.
- Capture:
- Findings lifecycle primitives exist (times_seen/first_seen/last_seen) and recurrence support exists (
findings.recurrence_key). - An existing recurrence-based drift generator exists:
App\Services\Drift\DriftFindingGenerator(usesrecurrence_keyand also setsfingerprint = recurrence_keyto satisfy the unique constraint). - Inventory sync is OperationRun-based and stamps
inventory_items.last_seen_operation_run_id.
Decisions
1) Finding identity for baseline compare
Decision: Baseline compare findings MUST use a stable recurrence key derived from:
tenant_idbaseline_snapshot_id(not baseline profile id)policy_typesubject_external_idchange_type
This recurrence key is stored in findings.recurrence_key and ALSO used as findings.fingerprint (to keep the existing unique constraint unique(tenant_id, fingerprint) effective).
Rationale:
- Matches Spec 116 (identity tied to
baseline_snapshot_idand independent of evidence hashes). - Aligns with existing, proven pattern in
DriftFindingGenerator(recurrence_key-based upsert; fingerprint reused).
Alternatives considered:
- Keep
DriftHasher::fingerprint(...)with baseline/current hashes included → rejected because it changes identity when evidence changes (violates FR-116v1-09). - Add a new unique DB constraint on
(tenant_id, recurrence_key)→ possible later hardening; not required initially because fingerprint uniqueness already enforces dedupe whenfingerprint = recurrence_key.
2) Scope key for baseline compare findings
Decision: Keep findings grouped by baseline profile using scope_key = baseline_profile:{baselineProfileId}.
Rationale:
- Spec 116 requires snapshot-scoped identity (via
baseline_snapshot_idin the recurrence key), but does not require snapshot-scoped grouping. - The repository already has UI widgets/stats and auto-close behavior keyed to
baseline_profile:{id}; keeping scope_key stable minimizes churn and preserves existing semantics. - Re-captures still create new finding identities because the recurrence key includes
baseline_snapshot_id.
Alternatives considered:
- Snapshot-scoped
scope_key = baseline_snapshot:{id}→ rejected for v1 because it would require larger refactors to stats, widgets, and auto-close queries, without being mandated by the spec.
3) Coverage guard (prevent false missing policies)
Decision: Coverage MUST be derived from the latest completed inventory_sync OperationRun for the tenant:
- Record per-policy-type processing outcomes into that run’s context (coverage payload).
- Baseline compare MUST compute
uncovered_policy_types = effective_scope - covered_policy_types. - Baseline compare MUST emit no findings of any kind for uncovered policy types.
- The compare OperationRun outcome should be
partially_succeededwhen uncovered types exist ("completed with warnings" in Ops UX), and summary counts should includeerrors_recorded = count(uncovered_policy_types).
Rationale:
- Spec FR-116v1-07 and SC-116-03.
- Current compare logic uses
inventory_items.last_seen_operation_run_idfilter only; without an explicit coverage list, a missing type looks identical to a truly empty tenant.
Alternatives considered:
- Infer coverage purely from "were there any inventory items for this policy type in the last sync run" → rejected because a legitimately empty type would be indistinguishable from "not synced".
4) Inventory meta contract hashing (v1 fidelity=meta)
Decision: Introduce an explicit "Inventory Meta Contract" builder used by BOTH capture and compare in v1.
- Inputs:
policy_type,external_id, and a whitelist of stable signals from inventory/meta (etag, last modified, scope tags, assignment target count, version marker when available). - Output: a normalized associative array, hashed deterministically.
Rationale:
- Spec FR-116v1-04: hashing must be based on a stable contract, not arbitrary meta.
- Current
BaselineSnapshotIdentity::hashItemContent()hashes the entiremeta_jsonb(including keys likeetagwhich may be noisy and keys that may expand over time).
Alternatives considered:
- Keep current hashing of
meta_jsonb→ rejected because it is not an explicit contract and may drift as we add inventory metadata.
5) Baseline scope + foundations
Decision: Extend baseline scope JSON to include:
policy_types: [](empty means default "all supported policy types excluding foundations")foundation_types: [](empty means default "none")
Foundations list must be derived from the same canonical foundation list used by inventory sync selection logic.
Rationale:
- Spec FR-116v1-01.
- Current
BaselineScopeonly supportspolicy_typesand treats empty as "all" (including foundations) which conflicts with the spec default.
6) v2 architecture strategy (content fidelity)
Decision: v2 is implemented as an extension of the same pipeline via a provider precedence chain:
PolicyVersion (if available) → Inventory content (if available) → Meta contract fallback (degraded)
The baseline compare engine stores dimension flags on the same finding (no additional finding identities).
Rationale:
- Spec FR-116v2-01 and FR-116v2-05.
- There is already a content-normalization + hashing stack in
DriftFindingGenerator(policy snapshot / assignments / scope tags) which can inform the content fidelity provider.
Notes / Risks
- Existing baseline compare findings are currently keyed by
fingerprintthat includes baseline/current hashes and usesscope_key = baseline_profile:{id}. The v1 migration should plan for “old findings become stale” behavior; do not attempt silent in-place identity rewriting without an explicit migration/backfill plan. - Coverage persistence must remain numeric-only in
summary_counts(per Ops-UX). Detailed coverage lists belong inoperation_runs.context.