spec(044): finalize drift MVP docs

This commit is contained in:
Ahmed Darrazi 2026-01-13 23:28:02 +01:00
parent f68da3b15f
commit df18cb1a0d
8 changed files with 614 additions and 24 deletions

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: Drift MVP (044)
**Purpose**: Validate specification completeness and quality before proceeding to implementation
**Created**: 2026-01-12
**Feature**: [specs/044-drift-mvp/spec.md](../spec.md)
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`.
- Constitution gate: this checklist must exist for features that change runtime behavior.

View File

@ -0,0 +1,146 @@
openapi: 3.0.3
info:
title: Admin Findings API (Internal)
version: 0.1.0
description: |
Internal contracts for the generic Findings pipeline.
Drift MVP is the first generator (finding_type=drift).
servers:
- url: /admin/api
paths:
/findings:
get:
summary: List findings
parameters:
- in: query
name: finding_type
schema:
type: string
enum: [drift, audit, compare]
- in: query
name: status
schema:
type: string
enum: [new, acknowledged]
- in: query
name: scope_key
schema:
type: string
- in: query
name: current_run_id
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Finding'
/findings/{id}:
get:
summary: Get finding detail
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Finding'
/findings/{id}/acknowledge:
post:
summary: Acknowledge a finding
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Finding'
/drift/generate:
post:
summary: Generate drift findings (async)
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
scope_key:
type: string
description: Inventory selection hash
required: [scope_key]
responses:
'202':
description: Accepted
components:
schemas:
Finding:
type: object
properties:
id:
type: integer
finding_type:
type: string
enum: [drift, audit, compare]
tenant_id:
type: integer
scope_key:
type: string
baseline_run_id:
type: integer
nullable: true
current_run_id:
type: integer
nullable: true
fingerprint:
type: string
subject_type:
type: string
subject_external_id:
type: string
severity:
type: string
enum: [low, medium, high]
status:
type: string
enum: [new, acknowledged]
acknowledged_at:
type: string
nullable: true
acknowledged_by_user_id:
type: integer
nullable: true
evidence_jsonb:
type: object
additionalProperties: true

View File

@ -0,0 +1,57 @@
# Phase 1 Design: Data Model (044)
## Entities
### Finding
New table: `findings`
**Purpose**: Generic, persisted pipeline for analytic findings (Drift now; Audit/Compare later).
**Core fields (MVP)**
- `id` (pk)
- `tenant_id` (fk tenants)
- `finding_type` (`drift` in MVP; later `audit`/`compare`)
- `scope_key` (string; deterministic; reuse Inventory selection hash)
- `baseline_run_id` (nullable fk inventory_sync_runs)
- `current_run_id` (nullable fk inventory_sync_runs)
- `fingerprint` (string; deterministic)
- `subject_type` (string; e.g. policy type)
- `subject_external_id` (string; Graph external id)
- `severity` (`low|medium|high`; MVP default `medium`)
- `status` (`new|acknowledged`)
- `acknowledged_at` (nullable)
- `acknowledged_by_user_id` (nullable fk users)
- `evidence_jsonb` (jsonb; sanitized, small; allowlist)
**Prepared for later (nullable, out of MVP)**
- `rule_id`, `control_id`, `expected_value`, `source`
## Constraints & Indexes
**Uniqueness**
- Unique: `(tenant_id, fingerprint)`
**Lookup indexes (suggested)**
- `(tenant_id, finding_type, status)`
- `(tenant_id, scope_key)`
- `(tenant_id, current_run_id)`
- `(tenant_id, baseline_run_id)`
- `(tenant_id, subject_type, subject_external_id)`
## Relationships
- `Finding` belongs to `Tenant`.
- `Finding` belongs to `User` via `acknowledged_by_user_id`.
- `Finding` belongs to `InventorySyncRun` via `baseline_run_id` (nullable) and `current_run_id` (nullable).
## Evidence shape (MVP allowlist)
For Drift MVP, `evidence_jsonb` should contain only:
- `change_type`
- `changed_fields` (list) and/or `change_counts`
- `run`:
- `baseline_run_id`, `current_run_id`
- `baseline_finished_at`, `current_finished_at`
No raw policy payload dumps; exclude secrets/tokens; exclude volatile fields for hashing.

View File

@ -1,24 +1,113 @@
# Implementation Plan: Drift MVP
# Implementation Plan: Drift MVP (044)
**Date**: 2026-01-07
**Spec**: `specs/044-drift-mvp/spec.md`
**Branch**: `feat/044-drift-mvp` | **Date**: 2026-01-12 | **Spec**: `specs/044-drift-mvp/spec.md`
**Input**: Feature specification from `specs/044-drift-mvp/spec.md`
## Summary
Add drift findings generation and UI using inventory and sync run metadata.
Introduce a generic, persisted Finding pipeline and implement Drift as the first generator.
## Dependencies
- Drift compares Inventory Sync Runs for the same selection scope (`scope_key`).
- Baseline run = previous successful run for the same scope; comparison run = latest successful run.
- Findings are persisted with deterministic fingerprints and support MVP triage (`new` → `acknowledged`).
- UI is DB-only for label/name resolution (no render-time Graph calls).
- Inventory core + run tracking (Spec 040)
- Inventory UI patterns (Spec 041)
## Technical Context
## Deliverables
**Language/Version**: PHP 8.4.x
**Framework**: Laravel 12
**Admin UI**: Filament v4 + Livewire v3
**Storage**: PostgreSQL (JSONB)
**Testing**: Pest v4
**Target Platform**: Docker (Sail-first local), Dokploy container deployments
**Project Type**: Laravel monolith
**Performance Goals**:
- Drift generation happens async (job), with deterministic output
- Drift listing remains filterable and index-backed
**Constraints**:
- Tenant isolation for all reads/writes
- No render-time Graph calls; labels resolved from DB caches
- Evidence minimization (sanitized allowlist; no raw payload dumps)
**Scale/Scope**:
- Tenants may have large inventories; findings must be indexed for typical filtering
- Baseline definition and drift finding generation
- Drift summary + detail UI
- Acknowledge/triage actions
## Constitution Check
## Risks
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- False positives if baseline definition is unclear
- Data volume for large tenants
- Inventory-first: Drift is derived from Inventory Sync Runs and Inventory Items (“last observed” state).
- Read/write separation: Drift generation is analytical; writes are limited to triage acknowledgement and must be audited + tested.
- Graph contract path: Drift UI performs no Graph calls; Graph calls remain isolated in existing Inventory/Graph client layers.
- Deterministic capabilities: drift scope derives from existing selection hashing and inventory type registries.
- Tenant isolation: all reads/writes tenant-scoped; no cross-tenant leakage.
- Automation: drift generation is queued; jobs are deduped/locked per scope+run pair and observable.
- Data minimization: store only minimized evidence JSON; logs contain no secrets/tokens.
## Project Structure
### Documentation (this feature)
```text
specs/044-drift-mvp/
├── plan.md # This file (/speckit.plan output)
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
│ └── admin-findings.openapi.yaml
└── tasks.md # Phase 2 output (/speckit.tasks)
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Pages/
│ │ └── DriftLanding.php # drift landing page (summary + generation status)
│ └── Resources/
│ └── FindingResource/ # list/detail + acknowledge action (tenant-scoped)
├── Jobs/
│ └── GenerateDriftFindingsJob.php # async generator (on-demand)
├── Models/
│ └── Finding.php # generic finding model
└── Services/
└── Drift/
├── DriftFindingGenerator.php # computes deterministic findings for baseline/current
├── DriftHasher.php # baseline_hash/current_hash helpers
└── DriftScopeKey.php # scope_key is InventorySyncRun.selection_hash (single canonical definition)
database/migrations/
└── 2026_.._.._create_findings_table.php
tests/Feature/Drift/
├── DriftGenerationDeterminismTest.php
├── DriftTenantIsolationTest.php
└── DriftAcknowledgeTest.php
```
**Structure Decision**: Laravel monolith using Filament pages/resources and queued jobs.
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| (none) | | |
## Phase 0 Output (Research)
Completed in `specs/044-drift-mvp/research.md`.
## Phase 1 Output (Design)
Completed in:
- `specs/044-drift-mvp/data-model.md`
- `specs/044-drift-mvp/contracts/`
- `specs/044-drift-mvp/quickstart.md`
## Phase 2 Planning Notes
Next step is expanding `specs/044-drift-mvp/tasks.md` (via `/speckit.tasks`) with phased, test-first implementation tasks.

View File

@ -0,0 +1,30 @@
# Quickstart: Drift MVP (044)
## Run locally (Sail)
```bash
./vendor/bin/sail up -d
./vendor/bin/sail artisan queue:work --tries=1
```
## Prepare data
1. Open the admin panel and select a tenant context.
2. Navigate to Inventory and run an Inventory Sync **twice** with the same selection (same `selection_hash`).
## Use Drift
1. Navigate to the new Drift area.
2. On first open, Drift will dispatch a background job to generate findings for:
- baseline = previous successful run for the same `scope_key`
- current = latest successful run for the same `scope_key`
3. Refresh the page once the job finishes.
## Triage
- Acknowledge a finding; it should move out of the “new” view but remain visible/auditable.
## Notes
- UI must remain DB-only for label resolution (no render-time Graph calls).
- Findings store minimal, sanitized evidence only.

View File

@ -0,0 +1,72 @@
# Phase 0 Output: Research (044)
## Decisions
### 1) `scope_key` reuse
- Decision: Use the existing Inventory selection hash as `scope_key`.
- Concretely: `scope_key = InventorySyncRun.selection_hash`.
- Rationale:
- Inventory already normalizes + hashes selection payload deterministically (via `InventorySelectionHasher`).
- It is already used for concurrency/deduping inventory runs, so its the right stable scope identifier.
- Alternatives considered:
- Compute a second hash (duplicate of selection_hash) → adds drift without benefit.
- Store the raw selection payload as the primary key → not stable without strict normalization.
### 2) Baseline selection (MVP)
- Decision: Baseline run = previous successful inventory sync run for the same `scope_key`; comparison run = latest successful inventory sync run for the same `scope_key`.
- Rationale:
- Matches “run at least twice” scenario.
- Deterministic and explainable.
- Alternatives considered:
- User-pinned baselines → valuable, but deferred (design must allow later via `scope_key`).
### 3) Persisted generic Findings
- Decision: Persist Findings in a generic `findings` table.
- Rationale:
- Enables stable triage (`acknowledged`) without recomputation drift.
- Reusable pipeline for Drift now, Audit/Compare later.
- Alternatives considered:
- Compute-on-demand and store only acknowledgements by fingerprint → harder operationally and can surprise users when diff rules evolve.
### 4) Generation trigger (MVP)
- Decision: On opening Drift, if findings for (tenant, `scope_key`, baseline_run_id, current_run_id) do not exist, dispatch an async job to generate them.
- Rationale:
- Avoids long request times.
- Avoids scheduled complexity in MVP.
- Alternatives considered:
- Generate after every inventory run → may be expensive; can be added later.
- Nightly schedule → hides immediacy and complicates operations.
### 5) Fingerprint and state hashing
- Decision: Use a deterministic fingerprint that changes when the underlying state changes.
- Fingerprint = `sha256(tenant_id + scope_key + subject_type + subject_external_id + change_type + baseline_hash + current_hash)`.
- baseline_hash/current_hash are computed over normalized, sanitized comparison data (exclude volatile fields like timestamps).
- Rationale:
- Stable identity for triage and audit.
- Supports future generators (audit/compare) using same semantics.
- Alternatives considered:
- Fingerprint without baseline/current hash → cannot distinguish changed vs unchanged findings.
### 6) Evidence minimization
- Decision: Store small, sanitized `evidence_jsonb` with an allowlist shape; no raw payload dumps.
- Rationale:
- Aligns with data minimization + safe logging.
- Avoids storing secrets/tokens.
### 7) Name resolution and Graph safety
- Decision: UI resolves human-readable labels using DB-backed Inventory + Foundations (047) + Groups Cache (051). No render-time Graph calls.
- Rationale:
- Works offline / when tokens are broken.
- Keeps UI safe and predictable.
## Notes / Follow-ups for Phase 1
- Define the `findings` table indexes carefully for tenant-scoped filtering (status, type, scope_key, run_ids).
- Consider using existing observable run patterns (BulkOperationRun + AuditLogger) for drift generation jobs.

View File

@ -20,6 +20,14 @@ ### Session 2026-01-12
- Q: Which inventory entities/types are in scope for Drift MVP? → A: Policies + Assignments.
- Q: When should drift findings be generated? → A: On-demand when opening Drift: if findings for (baseline,current,scope) dont exist yet, dispatch an async job to generate them.
### Session 2026-01-13
- Q: What should Drift do if there are fewer than two successful inventory runs for the same `scope_key`? → A: Show a blocked/empty state (“Need at least 2 successful runs for this scope to calculate drift”) and do not dispatch drift generation.
- Q: Should acknowledgement carry forward across comparisons? → A: No; acknowledgement is per comparison (`baseline_run_id` + `current_run_id` + `scope_key`). The same drift may re-appear as `new` in later comparisons.
- Q: Which `change_type` values are supported in Drift MVP? → A: `added`, `removed`, `modified` (assignment target/intent changes are covered under `modified`).
- Q: What is the default UI behavior for `new` vs `acknowledged` findings? → A: Default UI shows only `new`; `acknowledged` is accessible via an explicit filter.
- Q: What should the UI do if drift generation fails for a comparison? → A: Show an explicit error state (safe message + reference/run ids) and do not show findings for that comparison until a successful generation exists.
## Pinned Decisions (MVP defaults)
- Drift is implemented as a generator that writes persisted Finding rows (not only an in-memory/on-demand diff).
@ -65,6 +73,8 @@ ### Scenario 1: View drift summary
- When the admin opens Drift
- Then they see a summary of changes since the last baseline
- If there are fewer than two successful runs for the same `scope_key`, Drift shows a blocked/empty state and does not start drift generation.
### Scenario 2: Drill into a drift finding
- Given a drift finding exists
- When the admin opens the finding
@ -75,19 +85,23 @@ ### Scenario 3: Acknowledge/triage
- When the admin marks it acknowledged
- Then it is hidden from “new” lists but remains auditable
- Acknowledgement is per comparison; later comparisons may still surface the same drift as `new`.
## Functional Requirements
- FR1: Baseline + scope
- Define `scope_key` as a deterministic string derived from the Inventory Selection.
- Example: `scope_key = sha256(normalized selection payload)`.
- Must remain stable across equivalent selections (normalization), and allow future pinned baselines / compare baselines.
- Define `scope_key` as the deterministic Inventory selection identifier.
- MVP definition: `scope_key = InventorySyncRun.selection_hash`.
- Rationale: selection hashing already normalizes equivalent selections; reusing it keeps drift scope stable and consistent across the product.
- Baseline run (MVP) = previous successful inventory run for the same `scope_key`.
- Comparison run (MVP) = latest successful inventory run for the same `scope_key`.
- FR2: Finding generation (Drift MVP)
- Findings are persisted per (`baseline_run_id`, `current_run_id`, `scope_key`).
- Findings cover adds, removals, and metadata changes for supported entities (Policies + Assignments).
- Findings cover adds, removals, and changes for supported entities (Policies + Assignments).
- MVP `change_type` values: `added`, `removed`, `modified`.
- Findings are deterministic: same baseline/current + scope_key ⇒ same set of fingerprints.
- If fewer than two successful inventory runs exist for a given `scope_key`, Drift does not generate findings and must surface a clear blocked/empty state in the UI.
- FR2a: Fingerprint definition (MVP)
- Fingerprint = `sha256(tenant_id + scope_key + subject_type + subject_external_id + change_type + baseline_hash + current_hash)`.
@ -98,9 +112,13 @@ ## Functional Requirements
- Assignment drift includes target changes (e.g., groupId) and intent changes.
- FR3: Provide Drift UI with summary and details.
- Default lists and the Drift landing summary show only `status=new` by default.
- The UI must provide a filter to include `acknowledged` findings.
- If drift generation fails for a comparison, the UI must surface an explicit error state (no secrets), including reference identifiers (e.g., run ids), and must not fall back to stale/previous results.
- FR4: Triage (MVP)
- Admin can acknowledge a finding; record `acknowledged_by_user_id` + `acknowledged_at`.
- Acknowledgement does not carry forward across comparisons in the MVP.
- Findings are never deleted in the MVP.
## Non-Functional Requirements

View File

@ -1,7 +1,150 @@
# Tasks: Drift MVP
---
- [ ] T001 Define baseline and scope rules
- [ ] T002 Drift finding generation (deterministic)
- [ ] T003 Drift summary + detail UI
- [ ] T004 Acknowledge/triage state
- [ ] T005 Tests for determinism and tenant scoping
description: "Task list for feature 044 drift MVP"
---
# Tasks: Drift MVP (044)
**Input**: Design documents from `specs/044-drift-mvp/`
**Prerequisites**: `plan.md` (required), `spec.md` (required), plus `research.md`, `data-model.md`, `contracts/`, `quickstart.md`
**Tests**: REQUIRED (Pest) - feature introduces runtime behavior + new persistence.
**Organization**: Tasks are grouped by user story (Scenario 1/2/3 in spec).
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Project wiring for Drift MVP.
- [ ] T001 Create/update constitution gate checklist in `specs/044-drift-mvp/checklists/requirements.md`
- [ ] T002 Confirm spec/plan artifacts are current in `specs/044-drift-mvp/{plan.md,spec.md,research.md,data-model.md,quickstart.md,contracts/admin-findings.openapi.yaml}`
- [ ] T003 Add Drift landing page shell in `app/Filament/Pages/DriftLanding.php`
- [ ] T004 [P] Add Finding resource shells in `app/Filament/Resources/FindingResource.php` and `app/Filament/Resources/FindingResource/Pages/`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Persistence + authorization + deterministic IDs that all stories depend on.
**Checkpoint**: DB schema exists, tenant scoping enforced, and tests can create Finding rows.
- [ ] T005 Create `findings` migration in `database/migrations/*_create_findings_table.php` with Finding fields aligned to `specs/044-drift-mvp/spec.md`:
(tenant_id, finding_type, scope_key, baseline_run_id, current_run_id, subject_type, subject_external_id, severity, status, fingerprint unique, evidence_jsonb, acknowledged_at, acknowledged_by_user_id)
- [ ] T006 Create `Finding` model in `app/Models/Finding.php` (casts for `evidence_jsonb`, enums/constants for `finding_type`/`severity`/`status`, `acknowledged_at` handling)
- [ ] T007 [P] Add `FindingFactory` in `database/factories/FindingFactory.php`
- [ ] T008 Ensure `InventorySyncRunFactory` exists (or add it) in `database/factories/InventorySyncRunFactory.php`
- [ ] T009 Add authorization policy in `app/Policies/FindingPolicy.php` and wire it in `app/Providers/AuthServiceProvider.php` (or project equivalent)
- [ ] T010 Add Drift permissions in `config/intune_permissions.php` (view + acknowledge) and wire them into Filament navigation/actions
- [ ] T011 Enforce tenant scoping for Finding queries in `app/Models/Finding.php` and/or `app/Filament/Resources/FindingResource.php`
- [ ] T012 Implement fingerprint helper in `app/Services/Drift/DriftHasher.php` (sha256 per spec)
- [ ] T013 Pin `scope_key = InventorySyncRun.selection_hash` in `app/Services/Drift/DriftScopeKey.php` (single canonical definition)
- [ ] T014 Implement evidence allowlist builder in `app/Services/Drift/DriftEvidence.php` (new file)
---
## Phase 3: User Story 1 - View drift summary (Priority: P1) MVP
**Goal**: Opening Drift generates (async) and displays a summary of new drift findings for the latest scope.
**Independent Test**: With 2 successful inventory runs for the same selection hash, opening Drift dispatches generation if missing and then shows summary counts.
### Tests (write first)
- [ ] T015 [P] [US1] Baseline selection tests in `tests/Feature/Drift/DriftBaselineSelectionTest.php`
- [ ] T016 [P] [US1] Generation dispatch tests in `tests/Feature/Drift/DriftGenerationDispatchTest.php`
- [ ] T017 [P] [US1] Tenant isolation tests in `tests/Feature/Drift/DriftTenantIsolationTest.php`
- [ ] T018 [P] [US1] Assignment drift detection test (targets + intent changes per FR2b) in `tests/Feature/Drift/DriftAssignmentDriftDetectionTest.php`
### Implementation
- [ ] T019 [US1] Implement run selection service in `app/Services/Drift/DriftRunSelector.php`
- [ ] T020 [US1] Implement generator job in `app/Jobs/GenerateDriftFindingsJob.php` (dedupe/lock by tenant+scope+baseline+current)
- [ ] T021 [US1] Implement generator service in `app/Services/Drift/DriftFindingGenerator.php` (idempotent)
- [ ] T022 [US1] Implement landing behavior (dispatch + status UI) in `app/Filament/Pages/DriftLanding.php`
- [ ] T023 [US1] Implement summary queries/widgets in `app/Filament/Pages/DriftLanding.php`
---
## Phase 4: User Story 2 - Drill into a drift finding (Priority: P2)
**Goal**: Admin can view a finding and see sanitized evidence + run references (DB-only label resolution).
**Independent Test**: A persisted finding renders details without Graph calls.
### Tests (write first)
- [ ] T024 [P] [US2] Finding detail test in `tests/Feature/Drift/DriftFindingDetailTest.php`
- [ ] T025 [P] [US2] Evidence minimization test in `tests/Feature/Drift/DriftEvidenceMinimizationTest.php`
### Implementation
- [ ] T026 [US2] Implement list UI in `app/Filament/Resources/FindingResource.php` (filters: status, scope_key, run)
- [ ] T027 [US2] Implement detail UI in `app/Filament/Resources/FindingResource/Pages/ViewFinding.php`
- [ ] T028 [US2] Implement DB-only name resolution in `app/Filament/Resources/FindingResource.php` (inventory/foundations caches)
---
## Phase 5: User Story 3 - Acknowledge/triage (Priority: P3)
**Goal**: Admin can acknowledge findings; new lists hide acknowledged but records remain auditable.
**Independent Test**: Acknowledging sets `acknowledged_at` + `acknowledged_by_user_id` and flips status.
### Tests (write first)
- [ ] T029 [P] [US3] Acknowledge action test in `tests/Feature/Drift/DriftAcknowledgeTest.php`
- [ ] T030 [P] [US3] Acknowledge authorization test in `tests/Feature/Drift/DriftAcknowledgeAuthorizationTest.php`
### Implementation
- [ ] T031 [US3] Add acknowledge actions in `app/Filament/Resources/FindingResource.php`
- [ ] T032 [US3] Implement `acknowledge()` domain method in `app/Models/Finding.php`
- [ ] T033 [US3] Ensure Drift summary excludes acknowledged by default in `app/Filament/Pages/DriftLanding.php`
---
## Phase 6: Polish & Cross-Cutting Concerns
- [ ] T034 Add DB indexes in `database/migrations/*_create_findings_table.php` (tenant_id+status, tenant_id+scope_key, tenant_id+baseline_run_id, tenant_id+current_run_id)
- [ ] T035 [P] Add determinism test in `tests/Feature/Drift/DriftGenerationDeterminismTest.php` (same baseline/current => same fingerprints)
- [ ] T036 Add job observability logs in `app/Jobs/GenerateDriftFindingsJob.php` (no secrets)
- [ ] T037 Add idempotency/upsert strategy in `app/Services/Drift/DriftFindingGenerator.php`
- [ ] T038 Ensure volatile fields excluded from hashing in `app/Services/Drift/DriftHasher.php` and cover in `tests/Feature/Drift/DriftHasherTest.php`
- [ ] T039 Validate and update `specs/044-drift-mvp/quickstart.md` after implementation
---
## Dependencies & Execution Order
- Setup (Phase 1) -> Foundational (Phase 2) -> US1 -> US2 -> US3 -> Polish
### Parallel execution examples
- Foundational: T007, T008, T012, T013, T014
- US1 tests: T015, T016, T017, T018
- US2 tests: T024, T025
- US3 tests: T029, T030
---
## Implementation Strategy
### MVP scope
- MVP = Phase 1 + Phase 2 + US1.
### Format validation
- All tasks use `- [ ] T###` format
- Story tasks include `[US1]`/`[US2]`/`[US3]`
- All tasks include file paths