---

description: "Task list for Spec 118 implementation"

---

# Tasks: Golden Master Deep Drift v2 (Full Content Capture)

**Input**: Design documents from `/specs/118-baseline-drift-engine/`

**Tests**: REQUIRED (Pest) — this feature changes runtime behavior.

**Terminology**: `subject_key` = Spec 118 `normalized display_name` (trim + collapse internal whitespace + lowercase).

**Data isolation (SCOPE-001)**: Workspace-owned `baseline_snapshot_items` MUST NOT persist tenant identifiers (no tenant IDs, no tenant external IDs, no operation run IDs, no policy version IDs) — only cross-tenant keys + non-tenant metadata.

## Phase 1: Setup (Shared Infrastructure)

**Purpose**: Establish a safe baseline and introduce feature-level configuration scaffolding.

- [X] T001 Capture current baseline behavior by running existing suites in `tests/Feature/Baselines/BaselineCaptureTest.php`, `tests/Feature/Baselines/BaselineCompareFindingsTest.php`, `tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, and `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`
- [X] T002 [P] Add Spec 118 rollout + budget env vars to `.env.example` (e.g. `TENANTPILOT_BASELINE_FULL_CONTENT_CAPTURE_ENABLED`, `TENANTPILOT_BASELINE_EVIDENCE_MAX_ITEMS_PER_RUN=200`, `TENANTPILOT_BASELINE_EVIDENCE_MAX_CONCURRENCY=5`, `TENANTPILOT_BASELINE_EVIDENCE_MAX_RETRIES=3`, `TENANTPILOT_BASELINE_EVIDENCE_RETENTION_DAYS=90`)
- [X] T003 [P] Add config surface for Spec 118 rollout + budgets in `config/tenantpilot.php` (new `baselines.full_content_capture.*` keys sourced from env)

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Shared primitives required by ALL user stories.

**⚠️ CRITICAL**: No user story work should begin until this phase is complete.

- [X] T004 Add baseline capture mode enum in `app/Support/Baselines/BaselineCaptureMode.php` (values: `meta_only`, `opportunistic`, `full_content`)
- [X] T005 [P] Add policy version capture purpose enum in `app/Support/Baselines/PolicyVersionCapturePurpose.php` (values: `backup`, `baseline_capture`, `baseline_compare`)
- [X] T006 [P] Add subject-key helper in `app/Support/Baselines/BaselineSubjectKey.php` (normalize display name + derive workspace-safe subject id as `sha256(policy_type|subject_key)` for `baseline_snapshot_items.subject_external_id`)
- [X] T007 [P] Add baseline compare “why no findings” reason codes in `app/Support/Baselines/BaselineCompareReasonCode.php` (e.g. `no_subjects_in_scope`, `coverage_unproven`, `evidence_capture_incomplete`, `rollout_disabled`, `no_drift_detected`)
- [X] T008 [P] Add full-content rollout gate helper in `app/Support/Baselines/BaselineFullContentRolloutGate.php` (reads `config('tenantpilot.baselines.full_content_capture.enabled')`, provides an `assertEnabled()` used by services + jobs)
- [X] T009 [P] Add resume token contract in `app/Support/Baselines/BaselineEvidenceResumeToken.php` (versioned encode/decode; stored as opaque string in `operation_runs.context.*.resume_token`)
- [X] T010 [P] Add policy snapshot redactor in `app/Services/Intune/PolicySnapshotRedactor.php` (remove secrets/PII from payload/assignments/scope tags before persistence + hashing)
- [X] T011 [P] Add redaction coverage test in `tests/Feature/Intune/PolicySnapshotRedactionTest.php` (assert stored `PolicyVersion.snapshot` is redacted and content hash uses redacted content)

- [X] T012 Add migration for `baseline_profiles.capture_mode` in `database/migrations/2026_03_03_100001_add_capture_mode_to_baseline_profiles_table.php`
- [X] T013 [P] Add migration for `baseline_snapshot_items.subject_key` + index in `database/migrations/2026_03_03_100002_add_subject_key_to_baseline_snapshot_items_table.php`
- [X] T014 [P] Add migration for `policy_versions.capture_purpose`, `policy_versions.operation_run_id`, `policy_versions.baseline_profile_id` + indexes in `database/migrations/2026_03_03_100003_add_baseline_purpose_to_policy_versions_table.php`

- [X] T015 Update `app/Models/BaselineProfile.php` to store/cast `capture_mode` via `BaselineCaptureMode` and include it in `$fillable` (default: `opportunistic`)
- [X] T016 [P] Update factory defaults/states for capture mode in `database/factories/BaselineProfileFactory.php`
- [X] T017 [P] Update `database/factories/BaselineSnapshotItemFactory.php` to set `subject_key` derived from `meta_jsonb.display_name` via `BaselineSubjectKey` and set `subject_external_id` using the workspace-safe subject id (no tenant external IDs)
- [X] T018 Update `app/Models/PolicyVersion.php` to cast `capture_purpose` and define relationships to `OperationRun` + `BaselineProfile` (new nullable FKs)
- [X] T019 [P] Update `database/factories/PolicyVersionFactory.php` to default `capture_purpose` to `backup`

- [X] T020 Update `app/Services/Intune/VersionService.php` to apply `PolicySnapshotRedactor` before persistence/hashing and persist `capture_purpose`, `operation_run_id`, and `baseline_profile_id` when capturing versions (including via `captureFromGraph()`)
- [X] T021 Update `app/Services/Intune/PolicyCaptureOrchestrator.php` to pass baseline-purpose attribution into `PolicyVersion` creation/reuse/backfill and ensure snapshot dedupe uses redacted payloads (no secrets/PII in stored snapshots)

- [X] T022 Update content hashing to include settings + assignments + scope tags in `app/Services/Baselines/Evidence/ContentEvidenceProvider.php` (use `SettingsNormalizer`, hash normalized `assignments`, and hash normalized scope-tag IDs via `ScopeTagsNormalizer`)
- [X] T023 Ensure content evidence provenance includes `policy_version_id`, `operation_run_id`, and `capture_purpose` in `app/Services/Baselines/Evidence/ContentEvidenceProvider.php` (tenant-scoped only; snapshot items must strip tenant identifiers)

- [X] T024 Implement quota-aware baseline evidence capture phase scaffold in `app/Services/Baselines/BaselineContentCapturePhase.php` (inputs: tenant + subjects + purpose + budgets incl. concurrency + optional resume token; outputs: stats + gaps + optional resume token)

- [X] T025 Update run start context to include `target_scope` + `capture_mode` and enforce rollout gate for `full_content` in `app/Services/Baselines/BaselineCaptureService.php` (reject start if disabled)
- [X] T026 [P] Update run start context to include `target_scope` + `capture_mode` and enforce rollout gate for `full_content` in `app/Services/Baselines/BaselineCompareService.php` (reject start if disabled)

- [X] T027 Add capture mode field + badge to Filament baseline profile CRUD in `app/Filament/Resources/BaselineProfileResource.php` (hide/disable `full_content` option when rollout flag is disabled)

**Checkpoint**: DB + enums + capture phase scaffolding are in place; user stories can be implemented and tested independently.

---

## Phase 3: User Story 1 — Capture a full-content baseline without per-policy steps (Priority: P1) 🎯 MVP

**Goal**: Capture a baseline snapshot that uses full-content evidence by default (with explicit gaps + warnings if capture is incomplete).

**Independent Test**: Create a baseline profile configured for full-content capture, run “Capture baseline (full content)”, and validate the snapshot items have content-fidelity evidence (or explicit gaps) and the run context records capture stats.

### Tests (write first)

- [X] T028 [P] [US1] Add baseline full-content on-demand evidence test in `tests/Feature/BaselineDriftEngine/CaptureBaselineFullContentOnDemandTest.php` (no PolicyVersion exists → capture creates one with `capture_purpose=baseline_capture` and snapshot item fidelity is `content`)
- [X] T029 [P] [US1] Update meta-fallback test to assert opportunistic mode degrades to meta when evidence is missing in `tests/Feature/BaselineDriftEngine/CaptureBaselineMetaFallbackTest.php`
- [X] T030 [P] [US1] Update capture start surface expectations for full-content labeling + rollout gating in `tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`
- [X] T031 [P] [US1] Add snapshot item isolation test in `tests/Feature/BaselineDriftEngine/BaselineSnapshotNoTenantIdentifiersTest.php` (assert `baseline_snapshot_items` do not store tenant external IDs and `meta_jsonb` omits tenant identifiers like `meta_contract.subject_external_id` and `evidence.observed_operation_run_id`)
- [X] T032 [P] [US1] Add audit event coverage for baseline capture start/completion in `tests/Feature/BaselineDriftEngine/BaselineCaptureAuditEventsTest.php` (assert action metadata includes purpose, scope counts, and gap/warning summary)

### Implementation

- [X] T033 [US1] Update baseline capture action labeling + modal copy + rollout gate messaging in `app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` (show “Capture baseline (full content)” when `capture_mode=full_content`)
- [X] T034 [US1] Integrate `BaselineContentCapturePhase` into baseline capture in `app/Jobs/CaptureBaselineSnapshotJob.php` (purpose `baseline_capture`, budgeted, record `context.baseline_capture.evidence_capture`, `context.baseline_capture.gaps`, `context.baseline_capture.resume_token`, and add job-level rollout gate guard)
- [X] T035 [US1] Persist `subject_key` and workspace-safe `subject_external_id` (derived via `BaselineSubjectKey`) when building snapshot items, and sanitize `meta_jsonb` to exclude tenant identifiers in `app/Jobs/CaptureBaselineSnapshotJob.php`
- [X] T036 [US1] Update baseline snapshot identity hashing to use `policy_type + subject_key + baseline_hash` in `app/Services/Baselines/BaselineSnapshotIdentity.php` (dedupe must not depend on tenant-specific external IDs)
- [X] T037 [US1] Ensure capture run `status`/`outcome` transitions go through `OperationRunService` and mark warnings (`OperationRunOutcome::PartiallySucceeded`) when any subject falls back to meta or is skipped in `app/Jobs/CaptureBaselineSnapshotJob.php`
- [X] T038 [US1] Expand capture audit events to include purpose, scope counts, evidence capture stats, and gap/warning summary in `app/Jobs/CaptureBaselineSnapshotJob.php`
- [X] T039 [US1] Add snapshot fidelity + gaps counts into `baseline_snapshots.summary_jsonb` for snapshot list/detail UX in `app/Jobs/CaptureBaselineSnapshotJob.php`

**Parallel execution example (US1)**:

- Developer A: T028, T034, T036
- Developer B: T030, T033, T035, T038

**Checkpoint**: A baseline snapshot can be captured in full-content mode without per-policy steps, and runs are explainable when gaps exist.

---

## Phase 4: User Story 2 — Compare now with full content and get explainable drift (Priority: P1)

**Goal**: Compare baseline vs current using content-first evidence refresh, cross-tenant subject matching, and explainable run context.

**Independent Test**: Capture a full-content baseline, simulate a settings-only change for a subject, run “Compare now (full content)”, and assert a “different version” finding exists with content provenance.

### Tests (write first)

- [X] T040 [P] [US2] Add cross-tenant match test (policy_type + `subject_key`) in `tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php`
- [X] T041 [P] [US2] Add ambiguous match suppression test in `tests/Feature/Baselines/BaselineCompareAmbiguousMatchGapTest.php` (duplicate `subject_key` values → evidence gap; no finding)
- [X] T042 [P] [US2] Add coverage proof guard test in `tests/Feature/Baselines/BaselineCompareCoverageProofGuardTest.php` (uncovered types suppress `missing_policy` outcomes; run completes with warnings + records context)
- [X] T043 [P] [US2] Add stable recurrence identity test in `tests/Feature/Baselines/BaselineCompareFindingRecurrenceKeyTest.php` (recurrence key independent of hashes; retries don’t duplicate; lifecycle fields update)
- [X] T044 [P] [US2] Update compare start surface expectations for full-content labeling + rollout gating in `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`
- [X] T045 [P] [US2] Add baseline profile “Compare now (full content)” start-surface test in `tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`
- [X] T046 [P] [US2] Add audit event coverage for baseline compare start/completion in `tests/Feature/Baselines/BaselineCompareAuditEventsTest.php` (purpose, scope counts, gaps/warnings summary)

### Implementation

- [X] T047 [US2] Add “Compare now (full content)” header action to baseline profile view in `app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` (select target tenant; require `tenant.sync`; enforce rollout gate server-side)
- [X] T048 [US2] Integrate `BaselineContentCapturePhase` refresh into compare in `app/Jobs/CompareBaselineToTenantJob.php` (purpose `baseline_compare`, budgeted, record `context.baseline_compare.evidence_capture`, `context.baseline_compare.evidence_gaps`, `context.baseline_compare.resume_token`, and add job-level rollout gate guard)
- [X] T049 [US2] Switch compare matching to `policy_type + subject_key` in `app/Jobs/CompareBaselineToTenantJob.php` (load baseline items by `subject_key`; compute current `subject_key` from inventory display name; detect missing/empty/duplicate keys on either side; record gap reasons; suppress drift evaluation for those keys)
- [X] T050 [US2] Enforce coverage proof guard behavior in `app/Jobs/CompareBaselineToTenantJob.php` (suppress `missing_policy` for uncovered/unproven types; record warning + `BaselineCompareReasonCode` when suppression affects outcomes)
- [X] T051 [US2] Update finding recurrence identity to be stable and independent of hashes in `app/Jobs/CompareBaselineToTenantJob.php` (recurrence key uses tenant_id + baseline_profile_id + policy_type + subject_key + change_type; retries must not duplicate findings)
- [X] T052 [US2] Ensure findings carry `subject_key` + `display_name` fallbacks in `evidence_jsonb` and update subject display name fallback logic in `app/Filament/Resources/FindingResource.php` (COALESCE inventory display name with evidence display name)
- [X] T053 [US2] Ensure compare run context contains scope totals, processed counts, coverage proof status, fidelity breakdown, evidence capture stats, and top gap reasons in `app/Jobs/CompareBaselineToTenantJob.php`
- [X] T054 [US2] Update baseline compare landing to label “Compare now (full content)” when applicable in `app/Filament/Pages/BaselineCompareLanding.php` and `resources/views/filament/pages/baseline-compare-landing.blade.php`
- [X] T055 [US2] Extend stats DTO to surface fidelity + evidence gap summary from run context in `app/Support/Baselines/BaselineCompareStats.php`
- [X] T056 [US2] Add evidence capture + gaps panels for baseline capture/compare runs in Monitoring detail in `app/Filament/Resources/OperationRunResource.php`
- [X] T057 [US2] Expand compare audit events to include purpose, scope counts, evidence capture stats, and gaps/warnings summary in `app/Jobs/CompareBaselineToTenantJob.php`

**Parallel execution example (US2)**:

- Developer A: T040, T048, T050, T056
- Developer B: T045, T047, T054, T055, T052

**Checkpoint**: Compare runs refresh evidence when needed, generate findings reliably, and provide explainable context even with coverage warnings or gaps.

---

## Phase 5: User Story 3 — Throttling-safe, resumable evidence capture (Priority: P1)

**Goal**: Evidence capture respects quotas, records a resume token, and resumes deterministically without duplicating work.

**Independent Test**: Simulate throttling/budget exhaustion, verify run records a resume token, then resume and complete without re-capturing already-captured subjects.

### Tests (write first)

- [X] T058 [P] [US3] Add “budget exhaustion produces resume token” test in `tests/Feature/Baselines/BaselineCompareResumeTokenTest.php`
- [X] T059 [P] [US3] Add “resume is idempotent” test in `tests/Feature/Baselines/BaselineCompareResumeIdempotencyTest.php`
- [X] T060 [P] [US3] Add resume token contract test in `tests/Feature/Baselines/BaselineEvidenceResumeTokenContractTest.php` (token is opaque; decode yields deterministic resume state)
- [X] T061 [P] [US3] Add run-detail resume action test in `tests/Feature/Filament/OperationRunResumeCaptureActionTest.php`
- [X] T062 [P] [US3] Add audit event coverage for resume capture in `tests/Feature/Baselines/BaselineResumeCaptureAuditEventsTest.php`

### Implementation

- [X] T063 [US3] Implement budgets (items-per-run + concurrency + retries) + retry/backoff/jitter + throttling gap reasons + resume cursor handling in `app/Services/Baselines/BaselineContentCapturePhase.php` (use `BaselineEvidenceResumeToken` encode/decode)
- [X] T064 [US3] Add resume starter service in `app/Services/Baselines/BaselineEvidenceCaptureResumeService.php` (start follow-up `baseline_capture`/`baseline_compare` runs from a prior run + resume token; enforce RBAC; write audit events)
- [X] T065 [US3] Add “Resume capture” header action for eligible runs in `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` (requires confirmation; uses Ops-UX queued toast + canonical view-run link)
- [X] T066 [US3] Wire resume token consumption + re-emission into `app/Jobs/CaptureBaselineSnapshotJob.php` (baseline capture) and `app/Jobs/CompareBaselineToTenantJob.php` (baseline compare)

**Parallel execution example (US3)**:

- Developer A: T058, T063, T066
- Developer B: T061, T064, T065

**Checkpoint**: Operators can safely complete large scopes via resumable capture without manual per-policy capture.

---

## Phase 6: User Story 4 — “Why no findings?” is always clear (Priority: P2)

**Goal**: Zero findings never looks like a silent failure; compare run detail clearly explains the outcome.

**Independent Test**: Run compare with zero subjects (or with suppressed findings due to coverage/gaps) and verify a clear explanation sourced from run context is displayed.

### Tests (write first)

- [X] T067 [P] [US4] Add reason-code coverage test for zero-subject / zero-findings / suppressed-by-coverage outcomes in `tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php`
- [X] T068 [P] [US4] Add UI assertion test for “why no findings” messaging in `tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php`

### Implementation

- [X] T069 [US4] Populate `context.baseline_compare.reason_code` for all 0-subject / 0-findings outcomes in `app/Jobs/CompareBaselineToTenantJob.php` (use `BaselineCompareReasonCode`, including `coverage_unproven`/`rollout_disabled` where applicable)
- [X] T070 [US4] Render reason-code explanation + evidence context in Monitoring run detail in `app/Filament/Resources/OperationRunResource.php`
- [X] T071 [US4] Replace “All clear” copy with reason-aware messaging on baseline compare landing in `resources/views/filament/pages/baseline-compare-landing.blade.php` (source reason code from `BaselineCompareStats`)
- [X] T072 [US4] Propagate reason code + human message from run context in `app/Support/Baselines/BaselineCompareStats.php`

**Parallel execution example (US4)**:

- Developer A: T067, T069
- Developer B: T068, T071, T072

**Checkpoint**: Every compare run with “0 findings” has a clear, user-visible explanation and supporting evidence context.

---

## Phase 7: Polish & Cross-Cutting Concerns

**Purpose**: Guardrails, visibility, and validation across all stories.

- [X] T073 [P] Add Spec 118 no-legacy regression guard(s) in `tests/Feature/Guards/Spec118NoLegacyBaselineDriftGuardTest.php` (assert capture/compare do not implement hashing outside the provider/hasher pipeline and do not reference deprecated helpers)
- [X] T074 Update PolicyVersion listing to hide baseline-purpose evidence by default (unless the actor has `tenant.sync` or `tenant_findings.view`) in `app/Filament/Resources/PolicyVersionResource.php`
- [X] T075 [P] Add visibility/authorization coverage for baseline-purpose PolicyVersions in `tests/Feature/Filament/PolicyVersionBaselineEvidenceVisibilityTest.php` (assert baseline-purpose rows are hidden for `tenant.view`-only actors)
- [X] T076 Implement baseline-purpose PolicyVersion retention enforcement in `app/Console/Commands/PruneBaselineEvidencePolicyVersionsCommand.php` and schedule it in `routes/console.php` (prune `baseline_capture`/`baseline_compare` older than configured retention; do not prune `backup`) + tests in `tests/Feature/Retention/PruneBaselineEvidencePolicyVersionsTest.php` and `tests/Feature/Scheduling/PruneBaselineEvidencePolicyVersionsScheduleTest.php`
- [X] T077 Add Baseline Snapshot list/detail surfaces with fidelity visibility in `app/Filament/Resources/BaselineSnapshotResource.php`, `app/Filament/Resources/BaselineSnapshotResource/Pages/ListBaselineSnapshots.php`, and `app/Filament/Resources/BaselineSnapshotResource/Pages/ViewBaselineSnapshot.php` (badge + counts by fidelity; “captured with gaps” state) + tests in `tests/Feature/Filament/BaselineSnapshotFidelityVisibilityTest.php`
- [X] T078 Run formatting on changed files using `vendor/bin/sail bin pint --dirty --format agent` (touchpoints include `app/Jobs/CaptureBaselineSnapshotJob.php`, `app/Jobs/CompareBaselineToTenantJob.php`, `app/Services/Baselines/BaselineContentCapturePhase.php`)
- [X] T079 Run targeted test suite from `specs/118-baseline-drift-engine/quickstart.md` and update it if any step is inaccurate in `specs/118-baseline-drift-engine/quickstart.md`

---

## Dependencies & Execution Order

### Story completion order

- Phase 1 (Setup) → Phase 2 (Foundational) → user stories.
- User stories after Phase 2:
  - **US1 (P1)** is the MVP capture capability and should be implemented first end-to-end.
  - **US2 (P1)** depends on US1 for end-to-end validation (a baseline snapshot must exist), but implementation can proceed in parallel after Phase 2.
  - **US3 (P1)** depends on the capture phase being integrated in US1/US2.
  - **US4 (P2)** depends on US2’s run-context fields.

### Dependency graph

```mermaid
graph TD
  P1["Phase 1: Setup"] --> P2["Phase 2: Foundational"]
  P2 --> US1["US1: Capture baseline (full content)"]
  P2 --> US2["US2: Compare now (full content)"]
  US1 --> US2
  US2 --> US3["US3: Resumable capture"]
  US2 --> US4["US4: Why no findings"]
  US3 --> POLISH["Phase 7: Polish"]
  US4 --> POLISH
```

## Implementation Strategy (MVP-first)

1) Ship **US1** with a strict run-context contract and explicit gap reporting (no silent success).
2) Add **US2** compare refresh + cross-tenant matching with explainability.
3) Harden with **US3** resumability and throttle-safe behavior.
4) Complete operator trust with **US4** reason-code UX.
5) Enforce “no legacy” and visibility constraints in **Polish**.
