feat: Workspace settings slices v1 (backup, drift, operations) #120

Merged
ahmido merged 2 commits from 098-settings-slices-v1-backup-drift-ops into dev 2026-02-16 03:18:34 +00:00
4 changed files with 459 additions and 0 deletions
Showing only changes of commit d5d8d60017 - Show all commits

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: 098 — Settings Slices v1 (Backup + Drift + Operations)
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-16
**Feature**: [specs/098-settings-slices-v1-backup-drift-ops/spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass: 1/1
- This spec intentionally includes canonical setting keys and value constraints as part of the functional contract.

View File

@ -0,0 +1,142 @@
# Implementation Plan: 098 — Settings Slices v1 (Backup + Drift + Operations)
**Branch**: `098-settings-slices-v1-backup-drift-ops` | **Date**: 2026-02-16 | **Spec**: `specs/098-settings-slices-v1-backup-drift-ops/spec.md`
**Input**: Feature specification from `specs/098-settings-slices-v1-backup-drift-ops/spec.md`
## Summary
Extend the existing Settings Foundation to expose five additional workspace-level keys on the Workspace Settings page, with strict validation, per-setting reset-to-default (confirmed), and audit logging per key changed.
Apply those settings to three behavior paths:
- Backup retention: enforce a workspace-configurable retention default and a workspace-configurable retention floor (effective values clamped up to the floor).
- Drift severities: allow workspace-configurable `finding_type → severity` mapping (default `medium`, values normalized).
- Operations: allow workspace-configurable operation run retention days for pruning and store a “stuck run threshold” value (storage only; no new auto-remediation behavior).
## Technical Context
**Language/Version**: PHP 8.4 (Laravel 12)
**Primary Dependencies**: Filament v5, Livewire v4, Laravel Sail
**Storage**: PostgreSQL (Sail local)
**Testing**: Pest v4 (PHPUnit 12 runner)
**Target Platform**: Web app (Filament admin panel)
**Project Type**: Laravel monolith (Filament pages + services + jobs)
**Performance Goals**: Settings resolution should remain request-local cached (no repeated DB reads per key within a request).
**Constraints**:
- DB-only rendering for settings UI (no Graph calls as a render side-effect)
- Strict workspace isolation (non-member 404)
- Capability-gated mutations (member without capability 403)
- Destructive-like resets require confirmation
- Audit each successful mutation; multi-key save produces one audit entry per key changed
## Baselines to Preserve (When Unset)
These are the “no settings configured” behaviors that must remain unchanged and covered by regression tests.
Record the baseline values explicitly so “no change” remains mechanically verifiable over time:
- Backup retention default:
- When a schedule has no `retention_keep_last`, the job resolves `backup.retention_keep_last_default` and falls back to `30` if unresolved/non-numeric.
- Current clamp behavior: values < 1 are clamped up to 1.
- Source: `app/Jobs/ApplyBackupScheduleRetentionJob.php`.
- Current system default: `backup.retention_keep_last_default` is `30` in `app/Support/Settings/SettingsRegistry.php`.
- Drift default severity:
- Drift findings currently default to `Finding::SEVERITY_MEDIUM`.
- Source: `app/Services/Drift/DriftFindingGenerator.php`.
- Operation run pruning:
- Prune job default retention is `90` days (`new PruneOldOperationRunsJob()` with default constructor argument).
- Source: `app/Jobs/PruneOldOperationRunsJob.php` and schedule in `routes/console.php`.
- “Stuck run threshold”:
- No baseline behavior exists today; for this feature it remains storage-only (must not introduce auto-remediation).
## Constitution Check
*GATE: Must pass before implementation. Re-check after design/edits.*
- DB-only rendering: PASS (Workspace Settings UI is DB-only).
- Graph contract path: PASS (no Graph calls introduced).
- RBAC-UX semantics: PASS-BY-DESIGN (non-member 404; member missing capability 403; server-side still authoritative).
- Destructive-like confirmation: PASS-BY-DESIGN (per-setting reset actions must require confirmation).
- Auditability: PASS-BY-DESIGN (settings writes are audited per key).
- Filament Action Surface Contract: PASS (page-level action surface is explicitly declared in spec via UI Action Matrix).
## Project Structure
### Documentation (this feature)
```text
specs/098-settings-slices-v1-backup-drift-ops/
├── plan.md
├── spec.md
├── tasks.md
└── checklists/
└── requirements.md
```
### Source Code (repository root)
```text
app/
├── Filament/Pages/Settings/WorkspaceSettings.php
├── Jobs/
│ ├── ApplyBackupScheduleRetentionJob.php
│ └── PruneOldOperationRunsJob.php
├── Models/Finding.php
├── Services/Drift/DriftFindingGenerator.php
└── Support/
├── Badges/Domains/FindingSeverityBadge.php
└── Settings/SettingsRegistry.php
routes/console.php
tests/Feature/
├── BackupScheduling/BackupScheduleLifecycleTest.php
├── Drift/DriftPolicySnapshotDriftDetectionTest.php
├── Scheduling/PruneOldOperationRunsScheduleTest.php
└── SettingsFoundation/
├── WorkspaceSettingsManageTest.php
├── WorkspaceSettingsViewOnlyTest.php
└── WorkspaceSettingsNonMemberNotFoundTest.php
```
**Structure Decision**: Use existing Laravel structure only. No new top-level directories.
## Plan Phases
### Phase 0 — Align the Registry + UI primitives (shared)
- Ensure Settings registry rules match the spec (notably the backup max bounds).
- Refactor the Workspace Settings page to support:
- Per-setting reset actions (no global reset)
- Unset inputs with helper text showing the effective/default value
- Save semantics that can “unset” (delete override) for a single key
- Update the existing SettingsFoundation tests to reflect the new UX primitives.
### Phase 1 — US1 Backup slice (MVP)
- Add `backup.retention_min_floor` to the registry and UI.
- Apply floor clamping in the retention job to both schedule overrides and workspace defaults.
- Add/extend BackupScheduling + SettingsFoundation tests to cover both baseline behavior and clamping.
### Phase 2 — US2 Drift slice
- Add `drift.severity_mapping` to the registry and UI with strict JSON shape validation.
- Normalize severity values to lowercase on save; reject unsupported severities.
- Apply mapping in drift finding generation with default `medium` fallback.
- Add drift tests for default + mapped severity, and settings validation tests.
### Phase 3 — US3 Operations slice
- Add operations keys to the registry and UI.
- Wire pruning job to use the configured retention days when set (baseline otherwise).
- Ensure “stuck threshold” is stored only (no new behavior in this feature).
- Add pruning job/schedule tests and settings persistence tests.
### Phase 4 — Format + focused regression
- Run Pint for touched files.
- Run the focused test set for SettingsFoundation + BackupScheduling + Drift + Scheduling.
## Complexity Tracking
No constitution violations are required for this feature.

View File

@ -0,0 +1,160 @@
# Feature Specification: 098 — Settings Slices v1 (Backup + Drift + Operations)
**Feature Branch**: `098-settings-slices-v1-backup-drift-ops`
**Created**: 2026-02-16
**Status**: Draft
**Input**: Workspace-level settings slices for Backup, Drift severity mapping, and Operations retention/thresholds with safe defaults.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: Workspace Settings screen (admin UI)
- **Data Ownership**: workspace-owned settings values (key/value) + audit log entries for setting changes
- **RBAC**: workspace membership required; view vs manage capability-gated; deny-as-not-found for non-members
## Clarifications
### Session 2026-02-16
- Q: Which “Reset to default” scope do you want on the Workspace Settings UI? → A: Per-setting reset (each key individually).
- Q: When an admin clicks “Save” and multiple settings change at once, how should audit logging behave? → A: One audit log entry per key changed.
- Q: Should we enforce a validation constraint between `backup.retention_keep_last_default` and `backup.retention_min_floor`? → A: Allow any values; effective retention is always clamped to `backup.retention_min_floor`.
- Q: For settings that are currently unset (so the system uses defaults), how should the UI present them? → A: Leave input unset, show helper text with the default/effective value.
- Q: In `drift.severity_mapping`, should severity values be case-sensitive? → A: Case-insensitive; normalize to lowercase on save.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Configure backup retention defaults (Priority: P1)
As a workspace admin, I want to configure workspace-wide defaults for backup retention so that enterprise workspaces can tune policy without code/config changes.
**Why this priority**: Backup retention is a common enterprise requirement and impacts storage cost and governance.
**Independent Test**: Can be tested by setting/unsetting workspace settings and verifying backup retention behavior remains unchanged by default and changes deterministically when configured.
**Acceptance Scenarios**:
1. **Given** no workspace settings exist for backup retention, **When** a backup retention decision is made, **Then** behavior matches the current baseline (no change).
2. **Given** a workspace default retention value is configured, **When** a backup retention decision is made without a more specific override, **Then** the configured default is used.
3. **Given** a retention “floor” value is configured, **When** any calculated retention value is below the floor, **Then** the effective retention is clamped up to the floor.
4. **Given** a per-schedule override is configured, **When** that override is below the configured floor, **Then** the override is clamped up to the floor.
---
### User Story 2 - Configure drift severity mapping (Priority: P2)
As a workspace admin, I want to map drift finding types to severities so that findings align with enterprise risk posture and triage practices.
**Why this priority**: Severity directly affects how teams triage drift; a one-size-fits-all default is too rigid for enterprise.
**Independent Test**: Can be tested by saving a mapping, generating findings for mapped/unmapped types, and verifying severities are assigned correctly.
**Acceptance Scenarios**:
1. **Given** no drift severity mapping exists, **When** a drift finding is produced, **Then** its severity defaults to “medium”.
2. **Given** a mapping exists for a specific finding type, **When** a drift finding with that type is produced, **Then** the mapped severity is applied.
3. **Given** a mapping contains an unknown/unsupported severity value, **When** an admin attempts to save it, **Then** the save is rejected and no invalid values are persisted.
---
### User Story 3 - Configure operations retention and stuck threshold (Priority: P3)
As a workspace admin, I want to configure retention for operations/run records and store a “stuck run” threshold so that data lifecycle and operational heuristics are workspace-tunable.
**Why this priority**: Retention policies and operational thresholds vary widely between organizations and audit requirements.
**Independent Test**: Can be tested by saving retention/threshold settings and verifying the retention cutoff used by pruning changes accordingly while no new automatic actions occur.
**Acceptance Scenarios**:
1. **Given** no operations retention setting exists, **When** operations/run pruning is executed, **Then** the cutoff matches the current baseline (no change).
2. **Given** an operations retention setting exists, **When** pruning is executed, **Then** the cutoff is derived from the configured retention days.
3. **Given** a stuck threshold is configured, **When** the Workspace Settings screen is re-opened, **Then** the configured value is shown exactly as saved.
4. **Given** a stuck threshold is configured, **When** operations/run behavior is observed, **Then** no automatic remediation or auto-handling is performed in this feature scope.
### Edge Cases
- Attempting to save invalid numeric ranges (too low/high) is rejected and does not persist.
- Attempting to save invalid JSON (malformed) for drift mapping is rejected.
- Attempting to save drift mapping with non-string keys is rejected.
- Two admins editing settings concurrently results in deterministic persisted state (last write wins) and both attempts are auditable.
- A user without manage capability can view settings (read-only) but cannot submit changes.
- A non-member cannot discover the Workspace Settings screen or any values (deny-as-not-found).
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature MUST remain DB-only for screen rendering and MUST NOT introduce Microsoft Graph calls as part of rendering or saving these settings. Any setting mutation MUST be auditable.
### Dependencies & Assumptions
- This feature depends on an existing workspace settings foundation that provides: workspace-scoped storage, consistent defaults, centralized validation, RBAC capability enforcement, and audit logging for changes.
- No tenant-specific override UI is included in v1; only workspace-wide configuration is in scope.
- The default values for all settings keys match the current baseline behavior at the time this feature ships.
**Constitution alignment (RBAC-UX):**
- Authorization planes involved: workspace-scoped admin UI.
- Non-member / not entitled to workspace scope → 404 (deny-as-not-found).
- Member but missing manage capability → 403 on mutation attempts; UI remains read-only.
**Constitution alignment (OPS/observability):** This feature does not introduce a new long-running operation type; it changes which configuration values are used by existing behavior. All admin-initiated mutations MUST produce audit log entries.
### Functional Requirements
- **FR-001**: System MUST support workspace-level configuration for the following setting keys:
- `backup.retention_keep_last_default`
- `backup.retention_min_floor`
- `drift.severity_mapping`
- `operations.operation_run_retention_days`
- `operations.stuck_run_threshold_minutes`
- **FR-002**: System MUST preserve existing behavior when none of the above settings are configured (defaults MUST match the current baseline).
- **FR-003**: System MUST validate and reject invalid setting values, ensuring no invalid configuration is silently persisted.
- **FR-004**: Workspace Settings UI MUST present the above keys grouped into three sections (Backup, Drift, Operations) and MUST be fully functional without any external API calls.
- **FR-004a**: For any setting key that is currently unset, the UI MUST keep the input in an “unset” state and MUST display helper text indicating the default (currently effective) value.
- **FR-005**: Users with view capability MUST be able to view current effective settings but MUST NOT be able to change them.
- **FR-006**: Users with manage capability MUST be able to change settings and reset individual settings back to defaults.
- **FR-007**: Resetting a setting to default MUST be a confirmed (destructive-like) action.
- **FR-008**: System MUST write audit log entries for each settings update and reset-to-default event including: workspace identity, actor identity, setting key, old value, new value, and timestamp.
- **FR-008a**: When a single save operation changes multiple keys, the system MUST write one audit log entry per key changed.
#### Backup slice requirements
- **FR-009**: `backup.retention_keep_last_default` MUST be an integer between 1 and 365.
- **FR-010**: `backup.retention_min_floor` MUST be an integer between 1 and 365.
- **FR-011**: Effective backup retention MUST never be lower than `backup.retention_min_floor` (applies to defaults and any more specific overrides).
- **FR-011a**: The system MUST NOT reject configuration solely because `backup.retention_min_floor` exceeds `backup.retention_keep_last_default`; instead, the effective retention MUST be clamped to the floor.
#### Drift slice requirements
- **FR-012**: `drift.severity_mapping` MUST be a JSON object mapping `finding_type` (string) → severity.
- **FR-013**: Allowed severity values MUST be limited to: `low`, `medium`, `high`, `critical`.
- **FR-013a**: Severity values MUST be accepted case-insensitively and normalized to lowercase when persisted.
- **FR-014**: If a finding type has no mapping entry, severity MUST default to `medium`.
#### Operations slice requirements
- **FR-015**: `operations.operation_run_retention_days` MUST be an integer between 7 and 3650.
- **FR-016**: Pruning of operations/run records MUST use the configured retention days when set; otherwise it MUST behave as the baseline.
- **FR-017**: `operations.stuck_run_threshold_minutes` MUST be an integer between 0 and 10080.
- **FR-018**: `operations.stuck_run_threshold_minutes` MUST be a stored configuration value only; it MUST NOT introduce auto-remediation or auto-handling behavior in this feature scope.
## UI Action Matrix *(mandatory when Filament is changed)*
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Page | Workspace Settings (admin UI) | Save; Reset setting to default (per-setting, confirmed) | N/A | N/A | N/A | N/A | N/A | Save + Cancel | Yes | View capability: read-only fields; Manage capability: editable + submit; Non-member: 404 |
### Key Entities *(include if feature involves data)*
- **Workspace Setting**: A workspace-owned key/value configuration item with validation rules and a default.
- **Audit Log Entry**: An immutable record of a settings update or reset-to-default event.
- **Drift Finding Type**: A classification string used to identify the type of drift finding for severity mapping.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: When no settings are configured for these keys, backup retention behavior, drift severity behavior, and operations pruning behavior match the baseline in automated regression tests.
- **SC-002**: An authorized workspace admin can update each of the 5 settings and observe the new effective value reflected in the Workspace Settings screen immediately after save.
- **SC-003**: 100% of settings updates and reset-to-default actions produce an audit log entry with key, old value, and new value.
- **SC-004**: Invalid configuration attempts (out-of-range numbers, invalid JSON, unsupported severities) are rejected and do not change persisted settings.

View File

@ -0,0 +1,122 @@
---
description: "Task list for 098-settings-slices-v1-backup-drift-ops"
---
# Tasks: 098 — Settings Slices v1 (Backup + Drift + Operations)
**Input**: Design documents from `/specs/098-settings-slices-v1-backup-drift-ops/` (spec.md, plan.md)
**Tests**: REQUIRED (Pest) — runtime behavior changes.
**Scope**: workspace-level settings; DB-only rendering; no Graph calls.
## Phase 1: Setup (Shared Infrastructure)
- [X] T001 Confirm feature branch + clean working tree in specs/098-settings-slices-v1-backup-drift-ops/spec.md
- [X] T002 Verify Settings Foundation dependency is present by locating SettingsRegistry in app/Support/Settings/SettingsRegistry.php
- [X] T003 [P] Capture baseline behaviors + constants and record them in specs/098-settings-slices-v1-backup-drift-ops/plan.md by reviewing app/Jobs/ApplyBackupScheduleRetentionJob.php, app/Services/Drift/DriftFindingGenerator.php, app/Jobs/PruneOldOperationRunsJob.php, routes/console.php
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Shared settings primitives + Workspace Settings page patterns used by all slices.
- [X] T004 Update Settings registry validation to match spec (max 365) in app/Support/Settings/SettingsRegistry.php
- [X] T005 [P] Add per-setting reset UX pattern scaffolding (no global reset) in app/Filament/Pages/Settings/WorkspaceSettings.php
- [X] T006 Add “unset input + helper text shows default/effective” support in app/Filament/Pages/Settings/WorkspaceSettings.php
- [X] T007 [P] Update existing workspace settings RBAC tests for new per-setting reset actions in tests/Feature/SettingsFoundation/WorkspaceSettingsViewOnlyTest.php
- [X] T008 [P] Update existing manage test to assert per-setting reset (not header reset) in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
**Checkpoint**: Workspace Settings page supports per-setting resets and unset presentation without changing behavior.
---
## Phase 3: User Story 1 — Backup retention defaults (Priority: P1) 🎯 MVP
**Goal**: Workspace overrides for backup retention default + min floor; job clamps effective keep-last to floor.
**Independent Test**: Run ApplyBackupScheduleRetentionJob behavior with/without workspace overrides and verify clamping.
### Tests (US1)
- [X] T009 [P] [US1] Add/extend retention job tests to cover default + floor clamp in tests/Feature/BackupScheduling/BackupScheduleLifecycleTest.php
- [X] T010 [P] [US1] Add validation tests for backup settings bounds (1..365) via SettingsWriter in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
### Implementation (US1)
- [X] T011 [US1] Register backup floor setting and tighten keep-last rules (1..365) in app/Support/Settings/SettingsRegistry.php
- [X] T012 [US1] Extend Workspace Settings UI: Backup section adds `backup.retention_keep_last_default` + `backup.retention_min_floor` fields with per-setting reset actions in app/Filament/Pages/Settings/WorkspaceSettings.php
- [X] T013 [US1] Update Workspace Settings save logic: empty field triggers reset (delete override) instead of persisting null in app/Filament/Pages/Settings/WorkspaceSettings.php
- [X] T014 [US1] Apply floor clamping for both schedule override and resolved default in app/Jobs/ApplyBackupScheduleRetentionJob.php
---
## Phase 4: User Story 2 — Drift severity mapping (Priority: P2)
**Goal**: Workspace-level `finding_type → severity` mapping with default `medium` and strict validation; normalize severities to lowercase.
**Independent Test**: Generate drift findings and assert severity uses mapping when present; saving invalid mapping is rejected.
### Tests (US2)
- [X] T015 [P] [US2] Add drift generator test asserting default severity remains medium when no mapping set in tests/Feature/Drift/DriftPolicySnapshotDriftDetectionTest.php
- [X] T016 [P] [US2] Add drift generator test asserting mapped severity is applied when mapping exists in tests/Feature/Drift/DriftPolicySnapshotDriftDetectionTest.php
- [X] T017 [P] [US2] Add settings save validation tests for drift severity mapping JSON shape + allowed values in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
### Implementation (US2)
- [X] T018 [US2] Add `Finding::SEVERITY_CRITICAL` constant and ensure severity domain remains stable in app/Models/Finding.php
- [X] T019 [US2] Extend finding severity badge mapping to include `critical` (BADGE-001 compliant) in app/Support/Badges/Domains/FindingSeverityBadge.php
- [X] T020 [US2] Register `drift.severity_mapping` setting with JSON validation + canonical normalization (lowercase values, string keys) in app/Support/Settings/SettingsRegistry.php
- [X] T021 [US2] Update DriftFindingGenerator to resolve workspace severity mapping (via SettingsResolver in workspace context) and apply mapped severity (fallback medium) in app/Services/Drift/DriftFindingGenerator.php
- [X] T022 [US2] Extend Workspace Settings UI: Drift section adds JSON textarea for `drift.severity_mapping` with unset behavior + per-setting reset in app/Filament/Pages/Settings/WorkspaceSettings.php
---
## Phase 5: User Story 3 — Operations retention + stuck threshold (Priority: P3)
**Goal**: Workspace-level `operations.operation_run_retention_days` drives pruning; `operations.stuck_run_threshold_minutes` is stored only.
**Independent Test**: Create old/new OperationRuns across workspaces and verify prune respects per-workspace retention; stuck threshold persists and reloads.
### Tests (US3)
- [X] T023 [P] [US3] Update pruning schedule test to match new job behavior (per-workspace retention) in tests/Feature/Scheduling/PruneOldOperationRunsScheduleTest.php
- [X] T024 [P] [US3] Add prune job test verifying per-workspace retention cutoff using workspace settings in tests/Feature/Scheduling/PruneOldOperationRunsScheduleTest.php
- [X] T025 [P] [US3] Add workspace settings save/reset tests for operations keys in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
### Implementation (US3)
- [X] T026 [US3] Register operations settings keys with correct bounds in app/Support/Settings/SettingsRegistry.php
- [X] T027 [US3] Refactor PruneOldOperationRunsJob to compute retention per workspace (SettingsResolver + Workspace iteration) and prune by workspace_id in app/Jobs/PruneOldOperationRunsJob.php
- [X] T028 [US3] Ensure scheduler continues to enqueue prune job without needing a parameter in routes/console.php
- [X] T029 [US3] Extend Workspace Settings UI: Operations section adds `operation_run_retention_days` + `stuck_run_threshold_minutes` fields with unset behavior + per-setting reset in app/Filament/Pages/Settings/WorkspaceSettings.php
---
## Phase 6: Polish & Cross-Cutting Concerns
- [X] T030 [P] Confirm multi-key save emits one audit entry per key changed by reviewing app/Services/Settings/SettingsWriter.php and Workspace Settings save flow in app/Filament/Pages/Settings/WorkspaceSettings.php
- [X] T031 [P] Run Pint formatting on touched files via vendor/bin/sail bin pint --dirty (e.g., app/Filament/Pages/Settings/WorkspaceSettings.php, app/Support/Settings/SettingsRegistry.php, app/Jobs/ApplyBackupScheduleRetentionJob.php, app/Services/Drift/DriftFindingGenerator.php, app/Jobs/PruneOldOperationRunsJob.php)
- [X] T032 Run focused settings UI tests via vendor/bin/sail artisan test --compact tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
- [X] T033 Run focused drift tests via vendor/bin/sail artisan test --compact tests/Feature/Drift/DriftPolicySnapshotDriftDetectionTest.php
- [X] T034 Run focused pruning tests via vendor/bin/sail artisan test --compact tests/Feature/Scheduling/PruneOldOperationRunsScheduleTest.php
- [X] T035 [P] Add an automated regression test asserting per-setting reset actions require confirmation (destructive-like) in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
- [X] T036 [P] Add an automated regression test asserting multi-key save produces one audit entry per key changed (FR-008a) in tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
---
## Dependencies & Execution Order
- Setup (Phase 1) → Foundational (Phase 2) → US1 (Phase 3) → US2 (Phase 4) → US3 (Phase 5) → Polish (Phase 6)
## Parallel Execution Examples
```text
US1 parallel example: T009 + T011 + T014
US2 parallel example: T015 + T018 + T021
US3 parallel example: T023 + T027 + T029
```
## Implementation Strategy
- MVP = US1 only (backup defaults + floor clamp) with updated Workspace Settings UX and tests.
- Then US2 (drift mapping) and US3 (operations retention/threshold) as independent increments.