spec(086): retire legacy runs into operation runs

This commit is contained in:
Ahmed Darrazi 2026-02-10 01:35:24 +01:00
parent 0e2adeab71
commit 11f7209783
9 changed files with 747 additions and 2 deletions

View File

@ -43,9 +43,9 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 086-retire-legacy-runs-into-operation-runs: Spec docs updated (PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4)
- 084-verification-surfaces-unification: Added PHP 8.4 (Laravel 12) + Filament v5 (Livewire v4), Queue/Jobs (Laravel), Microsoft Graph via `GraphClientInterface` - 084-verification-surfaces-unification: Added PHP 8.4 (Laravel 12) + Filament v5 (Livewire v4), Queue/Jobs (Laravel), Microsoft Graph via `GraphClientInterface`
- 082-action-surface-contract: Added PHP 8.4.x + Laravel 12, Filament v5, Livewire v4 - 082-action-surface-contract: Added PHP 8.4.x + Laravel 12, Filament v5, Livewire v4
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

View File

@ -0,0 +1,34 @@
# Specification Quality Checklist: Retire Legacy Runs Into Operation Runs
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-09
**Feature**: [../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
- The spec uses product-specific terms (e.g., 404 vs 403 semantics) to make authorization behavior testable, but avoids naming specific frameworks or code-level implementation choices.

View File

@ -0,0 +1,41 @@
# Contracts (Spec 086)
This spec does not introduce a new public HTTP API surface.
## Canonical OperationRun contract (internal)
Spec 086 tightens and standardizes the internal contract for how operations are created, identified, and displayed.
### Run creation contract
- Start surfaces must create the `operation_runs` row **before** dispatching asynchronous work.
- Jobs must receive the `OperationRun` (or its id) and must **not** attempt a fallback-create.
### Identity / idempotency contract
Operation run identity is enforced by a partial unique index for active states.
Planned identity rules by type:
- `inventory.sync` and `directory_groups.sync`: deterministic identity (while-active dedupe)
- `backup_schedule.run_now` and `backup_schedule.retry`: unique-per-click identity (nonce)
- `backup_schedule.scheduled`: deterministic identity by `(backup_schedule_id, scheduled_for)` (strict)
### Context contract (selected keys)
The `operation_runs.context` JSON is used for:
- “Target” display (via `target_scope`)
- “Related” deep links (via `OperationRunLinks::related`)
- provenance (trigger source, schedule id, initiating user)
Keys referenced in existing UI code:
- `provider_connection_id`
- `backup_schedule_id`
- `backup_schedule_run_id`
- `restore_run_id`
- `target_scope`
## Graph Contract Registry
All Microsoft Graph calls remain required to go through `GraphClientInterface` and be modeled in `config/graph_contracts.php`.
Spec 086 removes Graph calls from Filament render/search/label callbacks (DB-only rendering), and moves those lookups behind cached tables + asynchronous sync operations.

View File

@ -0,0 +1,131 @@
# Data Model (Spec 086)
This feature consolidates execution tracking into `operation_runs` while keeping legacy run tables as read-only history.
## Entities
### 1) OperationRun (canonical)
**Table:** `operation_runs`
**Purpose:** Single source of truth for execution tracking: status/progress, results (counts), failures, provenance/context.
**Fields (current):**
- `id`
- `workspace_id` (FK, required)
- `tenant_id` (FK, nullable)
- `user_id` (FK, nullable)
- `initiator_name` (string)
- `type` (string; see OperationRunType registry)
- `status` (string; queued|running|completed)
- `outcome` (string; pending|succeeded|partially_succeeded|failed|blocked…)
- `run_identity_hash` (string; deterministic hash for idempotency)
- `summary_counts` (json/array; normalized counts + key metadata)
- `failure_summary` (json/array; structured failures, sanitized)
- `context` (json/array; provenance + inputs + target scope)
- `started_at`, `completed_at`, `created_at`, `updated_at`
**Indexes / constraints (current):**
- `(workspace_id, type, created_at)` and `(workspace_id, created_at)`
- `(tenant_id, type, created_at)` and `(tenant_id, created_at)`
- Partial unique indexes for active runs:
- tenant-scoped: unique `(tenant_id, run_identity_hash)` where `tenant_id IS NOT NULL` and `status IN ('queued','running')`
- workspace-scoped: unique `(workspace_id, run_identity_hash)` where `tenant_id IS NULL` and `status IN ('queued','running')`
**Context contract (current patterns):**
The `context` JSON is used for “related links” and display. Existing keys include (non-exhaustive):
- `provider_connection_id`
- `backup_schedule_id`
- `backup_schedule_run_id`
- `backup_set_id`
- `policy_id`
- `restore_run_id`
- `target_scope` (nested object)
- `selection` and `idempotency` objects for bulk operations
**Required additions for Spec 086 (planned):**
- New `type` values:
- `backup_schedule.scheduled`
- `directory_role_definitions.sync`
- Scheduled backup context keys:
- `backup_schedule_id`
- `scheduled_for` (UTC timestamp/minute)
- Optional `backup_schedule_run_id` if the legacy table remains for history during transition
### 2) InventorySyncRun (legacy)
**Table:** `inventory_sync_runs`
**Purpose:** Historical record (read-only) for pre-cutover tracking.
**Key fields:**
- `tenant_id`
- `selection_hash`
- `selection_payload` (nullable)
- status + timestamps + counters
**Planned optional mapping:**
- Add nullable `operation_run_id` FK to enable deterministic redirect to canonical viewer when present. No backfill required.
### 3) EntraGroupSyncRun (legacy)
**Table:** `entra_group_sync_runs`
**Purpose:** Historical record (read-only) for pre-cutover group sync tracking.
**Key fields:**
- `tenant_id`
- `selection_key`, `slot_key`
- status + error fields + counters
**Planned optional mapping:**
- Add nullable `operation_run_id` FK to enable deterministic redirect when present.
### 4) BackupScheduleRun (legacy)
**Table:** `backup_schedule_runs`
**Purpose:** Historical record of backup schedule executions.
**Planned behavior change:**
- Distinguish scheduled fires vs manual/retry at the OperationRun level by introducing `backup_schedule.scheduled` type.
**Planned optional mapping:**
- Add nullable `operation_run_id` FK to enable deterministic redirect when present.
### 5) RestoreRun (domain)
**Table:** `restore_runs`
**Purpose:** Domain workflow record (requested items, dry-run, preview/results). Execution tracking and “View run” uses `operation_runs`.
**Current linkage approach:**
- Canonical runs store `restore_run_id` in `operation_runs.context` (used by `OperationRunLinks::related`).
## Enumerations / Registries
### OperationRunType
**Location:** `app/Support/OperationRunType.php`
**Planned additions:**
- `BackupScheduleScheduled = 'backup_schedule.scheduled'`
- `DirectoryRoleDefinitionsSync = 'directory_role_definitions.sync'`
### OperationCatalog
**Location:** `app/Support/OperationCatalog.php`
**Planned additions:**
- Human label for `backup_schedule.scheduled`
- Human label for `directory_role_definitions.sync`
- Optional expected durations (if known)
## State transitions
### OperationRun
- `queued``running``completed`
- `outcome` starts as `pending`, transitions to one of: `succeeded`, `partially_succeeded`, `failed`, `blocked`.
The canonical update surface is `OperationRunService` (`dispatchOrFail`, `updateRun`, `appendFailures`, `incrementSummaryCounts`, etc.).

View File

@ -0,0 +1,109 @@
# Implementation Plan: Retire Legacy Runs Into Operation Runs
**Branch**: `086-retire-legacy-runs-into-operation-runs` | **Date**: 2026-02-10 | **Spec**: `specs/086-retire-legacy-runs-into-operation-runs/spec.md`
**Input**: Feature specification from `specs/086-retire-legacy-runs-into-operation-runs/spec.md`
## Summary
Retire legacy “run tracking” tables as the primary execution tracker for in-scope operations (inventory sync, directory groups sync, backup schedule runs, restore execution, and directory role definitions sync) and make `operation_runs` the canonical source of truth.
Key implementation approach:
- Use the existing tenantless canonical viewer `/admin/operations/{run}` (Filament page `TenantlessOperationRunViewer`) and ensure it remains DB-only at render time.
- Enforce the clarified 404/403 semantics for run viewing: non-members 404, members missing capability 403, where the view capability equals the start capability.
- Enforce dispatch-time OperationRun creation for every start surface; jobs never fallback-create.
- Apply explicit run identity rules per operation type (dedupe vs unique-per-click vs strict schedule dedupe), including strict scheduled backup idempotency: at most one canonical run ever per (schedule_id, intended fire-time).
- Remove Graph calls from UI render/search/label callbacks by using cached directory data (groups + role definitions) and “Sync now” operations.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4
**Storage**: PostgreSQL (via Sail)
**Testing**: Pest v4 (PHPUnit v12 runner)
**Target Platform**: Web application (Laravel + Filament admin panel)
**Project Type**: web
**Performance Goals**: Operations viewer + Monitoring pages render from DB state only; canonical viewer loads in ~2s under normal conditions
**Constraints**: No outbound HTTP in Monitoring/Operations rendering/search/label callbacks (OPS-EX-AUTH-001); dispatch-time OperationRun creation; jobs must never fallback-create; strict 404/403 isolation semantics
**Scale/Scope**: TenantPilot admin workflows; multiple operation families; staged cutover with legacy history preserved
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: No change to the meaning of inventory vs snapshots/backups; this spec only changes execution tracking.
- Read/write separation: Start surfaces remain enqueue-only; destructive-like actions are not added here; audit logging remains required for mutations.
- Graph contract path: UI render/search/label callbacks must become DB-only; any remaining Graph calls stay behind `GraphClientInterface` + `config/graph_contracts.php`.
- Deterministic capabilities: Run viewing must be capability-gated using the existing capability registry (no raw strings).
- RBAC-UX: Enforce clarified semantics for run viewing: non-members 404, members missing capability 403; authorization enforced server-side via Policy/Gate.
- Workspace isolation: Canonical tenantless `/admin/operations/{run}` continues to enforce workspace membership (deny-as-not-found).
- Global search: `OperationRunResource` stays non-globally-searchable; no new global-search surfaces introduced.
- Run observability: All in-scope long-running/scheduled/remote operations are tracked via `OperationRun`; Monitoring pages remain DB-only.
- Automation: Scheduled backup run creation uses strict idempotency per schedule + intended fire-time.
- Badge semantics (BADGE-001): Run status/outcome badges already use `BadgeRenderer`; do not introduce ad-hoc mappings.
- Filament UI Action Surface Contract: Legacy resources remain read-only; canonical operations pages already define inspection affordances.
## Project Structure
### Documentation (this feature)
```text
specs/086-retire-legacy-runs-into-operation-runs/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── README.md
└── tasks.md # To be created by /speckit.tasks
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Pages/
│ │ ├── Monitoring/
│ │ └── Operations/
│ └── Resources/
├── Http/
│ └── Middleware/
├── Jobs/
├── Models/
├── Policies/
├── Services/
└── Support/
config/
├── graph.php
└── graph_contracts.php
database/
└── migrations/
tests/
├── Feature/
└── Unit/
```
**Structure Decision**: Laravel web application (monolith) with Filament admin panel.
## Complexity Tracking
No constitution violations are required for this feature.
## Phase Plan
Phase 0/1 deliverables are already captured in:
- `specs/086-retire-legacy-runs-into-operation-runs/research.md`
- `specs/086-retire-legacy-runs-into-operation-runs/data-model.md`
- `specs/086-retire-legacy-runs-into-operation-runs/contracts/README.md`
- `specs/086-retire-legacy-runs-into-operation-runs/quickstart.md`
Phase 2 (tasks) will be produced via `/speckit.tasks` and should slice work by operation family:
1) Authorization: capability-gate canonical run viewing (404 vs 403 semantics).
2) Backup schedules: add `backup_schedule.scheduled` + strict idempotency; make manual runs unique-per-click.
3) Directory groups: stop writing legacy rows; keep legacy pages read-only; ensure dispatch-time OperationRun creation.
4) Inventory sync: stop writing legacy rows; ensure dispatch-time OperationRun creation and no UI Graph calls.
5) Tenant configuration: remove Graph calls from render/search/labels; add role definitions cache + “Sync now” operation.
6) Restore: ensure execution tracking uses OperationRun only; legacy restore domain records remain as domain entities.

View File

@ -0,0 +1,42 @@
# Quickstart (Spec 086)
This quickstart is for validating Spec 086 changes locally using Sail.
## Prereqs
- `vendor/bin/sail up -d`
## Run formatting
- `vendor/bin/sail bin pint --dirty`
## Run targeted tests
Use the minimal test subset relevant to the PR slice you are working on:
- `vendor/bin/sail artisan test --compact --filter=OperationRun`
- `vendor/bin/sail artisan test --compact tests/Feature` (narrow further to the new/changed files)
## Manual verification checklist
### Canonical viewer
- Trigger an operation that creates an `OperationRun`.
- Open the canonical URL (from notification action): `/admin/operations/{runId}`.
- Confirm the viewer renders from persisted DB state only.
### Authorization semantics
- As a non-workspace-member user, opening `/admin/operations/{runId}` returns 404.
- As a workspace member without the required capability for that run type, opening the viewer returns 403.
### Dedupe semantics
- Inventory sync / directory group sync: attempting to start while active reuses the existing active run and links to it.
- Manual backup schedule run now/retry: each click produces a distinct `OperationRun`.
- Scheduled backup: double-fire for the same schedule + intended minute produces at most one `OperationRun`.
### DB-only forms
- Tenant configuration selectors (directory groups, role definitions) render and search without outbound HTTP calls.
- “Sync now” actions enqueue operations and provide “View run” link.

View File

@ -0,0 +1,87 @@
# Research (Spec 086)
This document resolves the unknowns needed to write an implementation plan for “Retire Legacy Runs Into Operation Runs”. It is based on repository inspection (no new external dependencies).
## Decisions
### 1) Canonical run viewer is already implemented; keep the route shape
- **Decision:** Use the existing tenantless canonical viewer route `admin.operations.view` (path pattern `/admin/operations/{run}`) implemented by the Filament page `TenantlessOperationRunViewer`.
- **Rationale:** This already enforces “tenantless deep link” while still doing workspace / tenant entitlement checks server-side through `Gate::authorize('view', $run)`.
- **Alternatives considered:** Create a second viewer page or route. Rejected because it would introduce duplicate UX and increase the chance of policy drift.
Repository anchors:
- Canonical viewer page: `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
- Link helper: `app/Support/OperationRunLinks.php`
- Workspace selection middleware explicitly treats `/admin/operations/{id}` as workspace-optional: `app/Http/Middleware/EnsureWorkspaceSelected.php`
### 2) OperationRun is persisted and DB-rendered; schema supports workspace-tenant and workspace-only runs
- **Decision:** Treat `operation_runs` as the canonical persistence format for status/progress/results.
- **Rationale:** The schema already includes `workspace_id` (required) and `tenant_id` (nullable), enabling both tenant-plane and workspace-plane operations.
- **Alternatives considered:** Separate tables per operation family. Rejected because it breaks the Monitoring → Operations single source of truth principle.
Repository anchors:
- Migrations: `database/migrations/2026_01_16_180642_create_operation_runs_table.php`, `database/migrations/2026_02_04_090030_add_workspace_id_to_operation_runs_table.php`
- Model: `app/Models/OperationRun.php`
### 3) View authorization must be capability-gated per operation type (in addition to membership)
- **Decision:** Extend run viewing authorization to require the same capability used to start the operation type.
- **Rationale:** Spec 086 clarifications require: non-members get 404; members without capability get 403; and the “view” capability equals the “start” capability.
- **Implementation approach (planned):** Update `OperationRunPolicy::view()` to:
1) Keep existing workspace membership and tenant entitlement checks (deny-as-not-found).
2) Resolve required capability from `OperationRun->type` using a centralized mapping helper.
3) If capability is known and tenant-scoped, enforce `403` when the member lacks it.
Repository anchors:
- Current policy (membership + tenant entitlement only): `app/Policies/OperationRunPolicy.php`
- Existing capability enforcement in start surfaces (examples):
- Inventory sync start: `Capabilities::TENANT_INVENTORY_SYNC_RUN` in `app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php`
- Directory groups sync start: `Capabilities::TENANT_SYNC` in `app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php`
- Backup schedule run/retry: `Capabilities::TENANT_BACKUP_SCHEDULES_RUN` in `app/Filament/Resources/BackupScheduleResource.php`
### 4) Run identity / dedupe strategy varies by operation type
- **Decision:** Use existing `OperationRunService` helpers but apply type-specific identity rules:
- `inventory.sync` and `directory_groups.sync`: **while-active dedupe** based on deterministic inputs (continue using `ensureRun(...)`-style identity).
- `backup_schedule.run_now` and `backup_schedule.retry`: **unique per click** (no dedupe). Create a new run each time by including a nonce in identity inputs (e.g., UUID).
- `backup_schedule.scheduled`: **strict dedupe** per `(backup_schedule_id, scheduled_for)`; create a new operation type `backup_schedule.scheduled` and use `ensureRunWithIdentity(...)` keyed by schedule + intended fire-time.
- **Rationale:** Matches explicit spec clarifications and protects against scheduler double-fire.
- **Alternatives considered:**
- Keep using `ensureRun(...)` for manual runs → rejected (dedupes while active).
- Use legacy table unique constraints as idempotency → rejected (spec requires OperationRun is canonical).
Repository anchors:
- `ensureRun(...)` and `ensureRunWithIdentity(...)`: `app/Services/OperationRunService.php`
- Existing partial unique index for active runs: `operation_runs_active_unique_*` in the migrations above.
### 5) Legacy run tables are real and currently written to; deterministic redirect requires an explicit mapping field
- **Decision:** Legacy tables remain viewable and read-only, but should not be relied on for current execution tracking.
- **Rationale:** Spec requires “no new legacy rows” for in-scope operations. Today, some start surfaces still create legacy rows (e.g., inventory/group sync, backup schedule runs).
- **Planned design:**
- Stop creating new legacy rows as part of the cutover PRs.
- Implement legacy “view” redirect behavior only when a record has a canonical mapping.
- To make redirects deterministic without a backfill, add an optional `operation_run_id` FK column to legacy tables that we intend to redirect (only populated for rows created after the migration; older rows remain legacy-only view).
- **Alternatives considered:** Derive mapping by recomputing hashes and searching by time window. Rejected as non-deterministic and likely to pick the wrong run when identities collide historically.
Repository anchors (legacy tables):
- Inventory sync runs: `database/migrations/2026_01_07_142719_create_inventory_sync_runs_table.php`
- Directory group sync runs: `database/migrations/2026_01_11_120004_create_entra_group_sync_runs_table.php`
- Backup schedule runs: `database/migrations/**create_backup_schedule_runs**` (used in `BackupScheduleResource`)
- Restore runs (domain): `database/migrations/2025_12_10_000150_create_restore_runs_table.php`
### 6) DB-only rendering constraint is already enforced in Monitoring pages, but Tenant configuration forms still call Graph
- **Decision:** Remove outbound Graph calls from configuration-form search/labels by introducing cached directory role definitions and using cached directory groups.
- **Rationale:** Constitution OPS-EX-AUTH-001 + Spec 086 FR-006/FR-015 require render/search/label resolution to be DB-only.
- **Repository finding:** `TenantResource` currently queries Graph for role definitions in selector callbacks.
Repository anchors:
- Graph call sites inside UI callbacks: `app/Filament/Resources/TenantResource.php` (roleDefinitions search/label methods)
## Open items (resolved enough for planning)
- Exact schema for the new role definition cache tables and the sync job contract will be specified in `data-model.md` and implemented in Phase PR(s).
- The capability mapping for run viewing will be implemented via a centralized helper; the plan will enumerate required capabilities per in-scope operation type.

View File

@ -0,0 +1,160 @@
# Feature Specification: Retire Legacy Runs Into Operation Runs
**Feature Branch**: `086-retire-legacy-runs-into-operation-runs`
**Created**: 2026-02-09
**Status**: Draft
**Input**: User description: "Retire legacy run tracking into canonical operation runs, with DB-only rendering and dispatch-time run creation. Legacy run tables remain read-only history."
## Clarifications
### Session 2026-02-10
- Q: For manual backup schedule runs (`backup_schedule.run_now`) and retries (`backup_schedule.retry`), should the system dedupe while a run is active, or always create a new run per click? → A: Always create a new run per click (no dedupe).
- Q: Who may view the canonical run detail page (“View run”)? → A: Workspace members may view runs only if they also have the required capability for that operation type; non-members get 404, members without capability get 403.
- Q: Which capability should be required to view a run (“View run”)? → A: Use the same capability as starting that operation type.
- Q: For `backup_schedule.scheduled`, how should dedupe work? → A: Strict dedupe per schedule and intended fire-time (at most one run).
- Q: For the role definitions cache “Sync now” operation, should it use a new dedicated operation type or reuse an existing one? → A: Use a new dedicated operation type.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Start an operation with an immediate canonical run link (Priority: P1)
As a workspace member, I can start long-running operations (inventory sync, directory groups sync, scheduled backups, restore execution, directory role definitions sync) and immediately receive a stable “View run” link that I can open and share.
**Why this priority**: This removes the “run link appears later / changes” ambiguity, improves auditability, and prevents duplicate tracking paths.
**Independent Test**: Trigger each supported operation start surface and verify a canonical run record exists before work begins, and that the canonical viewer loads from persisted state.
**Acceptance Scenarios**:
1. **Given** a workspace member with the required capability, **When** they start an inventory sync, **Then** a canonical run exists immediately and the UI shows a stable “View run” link.
2. **Given** a scheduled backup fire event, **When** the scheduler dispatches work, **Then** a canonical run exists immediately and the same fire event cannot create duplicates.
3. **Given** a workspace member without the required capability, **When** they attempt to start the operation, **Then** the request is rejected with a capability error (403) and no run is created.
---
### User Story 2 - Monitor executions from a single canonical viewer (Priority: P2)
As a workspace member, I can open an operations viewer link for any run and see status, progress, results, and errors without the page triggering outbound calls.
Legacy “run history” pages remain available for older historical rows but cannot start or retry anything.
**Why this priority**: A single viewer reduces support load, enables consistent deep linking, and avoids UI latency and rate-limiting from outbound calls.
**Independent Test**: Load the canonical viewer and legacy history pages using outbound client fakes/mocks and assert no outbound calls occur during rendering/search.
**Acceptance Scenarios**:
1. **Given** a run exists, **When** a user opens its canonical operations link, **Then** the page renders only from persisted state and performs no outbound calls.
2. **Given** a legacy run history record that has a known canonical mapping, **When** a user opens the legacy “view” page, **Then** they are redirected to the canonical operations viewer.
3. **Given** a legacy run history record without a canonical mapping, **When** a user opens the legacy “view” page, **Then** they see a read-only historical record and no new canonical run is created.
---
### User Story 3 - Use cached directory data in forms without blocking calls (Priority: P3)
As a workspace member configuring tenant-related settings, I can search/select directory groups and role definitions using cached data. If cached data is missing or stale, I can trigger an asynchronous sync (“Sync now”) without the form making outbound calls.
**Why this priority**: Prevents slow, flaky UI and rate-limits from inline lookups, while keeping the configuration flow usable.
**Independent Test**: Render the configuration form and exercise search/label rendering while asserting outbound clients are not called.
**Acceptance Scenarios**:
1. **Given** cached directory groups exist, **When** the user searches for groups, **Then** results and labels come from cached data.
2. **Given** cached role definitions are missing, **When** the user opens the role definition selector, **Then** the UI indicates “data not available yet” and offers a non-destructive “Sync now” action.
3. **Given** the user triggers “Sync now”, **When** the sync starts, **Then** a canonical run is created immediately and the user can open its canonical “View run” link.
### Edge Cases
- A scheduler fires the same scheduled backup more than once for the same intended time.
- A user triggers the same sync while an identical sync is still active (dedupe/while-active semantics).
- A job fails before writing progress; the canonical run still exists and shows a clear failure state.
- A legacy history row exists but has no canonical mapping; it must remain viewable without creating new canonical runs.
- A non-member attempts to access a canonical operations link; response must be deny-as-not-found (404).
- A member lacks capability: start surfaces must reject (403) and the UI must reflect disabled affordances.
- Cached directory data is empty or stale; UI must not block on outbound calls and must provide a safe way to sync.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature includes long-running/queued/scheduled work. The spec MUST describe tenant isolation, run observability (type/identity/visibility), and tests.
**Constitution alignment (RBAC-UX):** This feature changes authorization behavior and navigation paths. It MUST define 404 vs 403 semantics and ensure server-side enforcement for operation-start flows.
**Constitution alignment (OPS-EX-AUTH-001):** Outbound HTTP without a canonical run is not allowed on Monitoring/Operations pages.
**Constitution alignment (BADGE-001):** Any new/changed status presentation for runs MUST remain centralized and covered by tests.
**Constitution alignment (Admin UI Action Surfaces):** This feature changes multiple admin UI surfaces and MUST satisfy the UI Action Surface Contract (see matrix below).
### Functional Requirements
- **FR-001 (Canonical tracking)**: The system MUST treat the canonical run record as the single source of truth for execution tracking (status, progress, results, errors) for the in-scope operations.
- **FR-002 (Dispatch-time creation)**: Every start surface (UI action, console command, scheduler, internal service) MUST create the canonical run record before dispatching any asynchronous work.
- **FR-003 (No job fallback-create)**: Background workers MUST NOT create canonical run records as a fallback; missing run identifiers are treated as a fatal contract violation.
- **FR-004 (Canonical deep-link)**: The system MUST support exactly one canonical deep-link format for viewing runs which is tenantless and stable.
- **FR-005 (Membership + capability rules)**: Access to operation runs MUST follow these rules:
- Non-members of the workspace scope MUST receive deny-as-not-found (404).
- Workspace members who lack the required capability for the operation type MUST receive 403.
- **FR-005a (View capability mapping)**: “View run” MUST require the same capability as “Start” for the corresponding operation type.
- **FR-006 (DB-only rendering)**: Operations/monitoring and run viewer pages MUST render solely from persisted data and MUST NOT perform outbound calls during rendering/search/label resolution.
- **FR-007 (Legacy history read-only)**: Legacy run history records MUST remain viewable as historical data, but MUST be strictly read-only (no start/retry/execute actions).
- **FR-008 (Legacy redirects)**: If a legacy history record includes a canonical mapping, the legacy “view” page MUST redirect deterministically to the canonical viewer; otherwise it MUST display legacy-only history.
- **FR-009 (No new legacy rows)**: For the in-scope operations, the system MUST stop writing new legacy run history rows. Existing legacy history remains unchanged.
- **FR-010 (Scheduled backup classification)**: Scheduled backup executions MUST be represented with a distinct operation type (not conflated with manual runs).
- **FR-011 (Run identity & dedupe)**: The system MUST compute deterministic run identities for dedupe and scheduler double-fire protection, and MUST define whether each type dedupes “while active” or is strictly unique.
- **FR-011b (Scheduled backups are strict)**: Scheduled backup executions MUST use strict dedupe per schedule and intended fire-time (at most one canonical run ever per schedule per intended fire-time).
- **FR-011a (Backup manual runs are unique)**: Manual backup schedule runs (“run now”) and retries MUST be unique per user action (no while-active dedupe).
- **FR-012 (Inputs & provenance)**: The system MUST store operation inputs and provenance (target tenant/schedule, trigger source, optional initiating user) on the canonical run record.
- **FR-013 (Structured results)**: The system MUST store a standard, structured summary of results (counts) and failures (structured error entries) on the canonical run record.
- **FR-014 (Restore domain vs execution)**: Restore workflow domain records may remain as domain entities, but execution tracking and “View run” affordances MUST use the canonical run record exclusively.
- **FR-015 (Cached directory data)**: The system MUST provide cached directory group data and cached role definition data to support search and label rendering in configuration forms without outbound calls.
- **FR-015a (Role definitions sync type)**: The role definitions cache sync MUST use a dedicated operation type (e.g., `directory_role_definitions.sync`) to keep identities, results, and auditability distinct from other sync operations.
- **FR-016 (Safe “Sync now”)**: When cached directory data is missing, the UI MUST provide a non-destructive “Sync now” action that starts an asynchronous sync and immediately exposes the canonical run link.
#### Assumptions
- A canonical run model/viewer already exists and is suitable for monitoring long-running operations.
- Outbound calls to external services are permitted only in asynchronous execution paths and are observable via the canonical run record.
#### Out of Scope
- Backfilling legacy history into canonical runs.
- Dropping/removing legacy run history tables.
- Introducing new cross-workspace analytics.
## UI Action Matrix *(mandatory when admin UI 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Operations viewer | Canonical run viewer route | None | Open by canonical link | None | None | None | None | N/A | Yes (canonical run record metadata) | Must be DB-only rendering; non-member is 404 |
| Inventory sync start | Inventory admin UI | Start sync | View run link appears after start | View run | None | None | N/A | N/A | Yes | Capability-gated; creates canonical run before dispatch |
| Directory groups sync start | Directory groups admin UI & console | Sync now | View run link appears after start | View run | None | Sync now (when cache empty) | N/A | N/A | Yes | Single dispatcher entry; legacy start actions removed |
| Backup schedule runs list | Backup schedule detail | None | List links open canonical viewer | View run | None | None | N/A | N/A | Yes | Includes scheduled/manual/retry runs; scheduled has distinct type |
| Tenant configuration selectors | Tenant settings forms | Sync now (when cache empty) | Search from cached data | None | None | Sync now | N/A | Save/Cancel | Yes | No outbound calls in search/label resolution |
| Legacy run history pages | Archive/history areas | None | View (read-only) | View only | None | None | None | N/A | Yes (historical) | No Start/Retry; redirect only if canonical mapping exists |
### Key Entities *(include if feature involves data)*
- **Canonical Run**: A single, shareable execution record containing type, identity, provenance, status, progress, results, and errors.
- **Legacy Run History Record**: A historical record for prior run-tracking paths; viewable but not mutable.
- **Managed Tenant**: The tenant context targeted by operations.
- **Backup Schedule**: A schedule configuration that can trigger executions automatically.
- **Restore Run (Domain Record)**: The domain workflow record for restore; links to canonical execution runs.
- **Directory Group Cache**: Cached group metadata used for searching/label rendering in forms.
- **Role Definition Cache**: Cached role definition metadata used for searching/label rendering in forms.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 100% of newly started in-scope operations create a canonical run record before any asynchronous work is dispatched.
- **SC-002**: Over a 30-day staging observation window, 0 new legacy run history rows are created for in-scope operations.
- **SC-003**: Operations viewer and monitoring pages perform 0 outbound calls during rendering/search/label resolution (verified by automated tests).
- **SC-004**: For scheduled backups, duplicate scheduler fires for the same schedule and intended fire-time result in at most 1 canonical run.
- **SC-005**: Users can open a canonical “View run” link and see status/progress within 2 seconds in typical conditions.

View File

@ -0,0 +1,141 @@
---
description: "Task list for Spec 086 implementation"
---
# Tasks: Retire Legacy Runs Into Operation Runs (086)
**Input**: Design documents from `specs/086-retire-legacy-runs-into-operation-runs/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/, quickstart.md
**Tests**: REQUIRED (Pest) — runtime behavior changes must be covered.
## Phase 1: Setup (Shared Infrastructure)
- [ ] T001 Confirm baseline green test subset via `tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `tests/Feature/Monitoring/OperationsDbOnlyTest.php`, and `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php`
- [ ] T002 Confirm Filament v5 + Livewire v4 constraints are respected for any touched pages/resources in `app/Filament/**`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Shared primitives required by all stories.
- [ ] T003 Add centralized “run type → required capability” resolver in `app/Support/Operations/OperationRunCapabilityResolver.php`
- [ ] T004 Update `app/Policies/OperationRunPolicy.php` to enforce clarified 404/403 semantics (non-member 404; member missing capability 403) using T003
- [ ] T005 [P] Add/extend operation type registry for new types in `app/Support/OperationRunType.php`
- [ ] T006 [P] Add/extend operation labels/catalog entries in `app/Support/OperationCatalog.php`
- [ ] T007 Add tests covering view authorization semantics in `tests/Feature/Operations/TenantlessOperationRunViewerTest.php` (404 vs 403 + capability-gated view)
**Checkpoint**: Canonical viewer authorization matches spec; new run types exist in registries.
---
## Phase 3: User Story 1 — Start an operation with an immediate canonical run link (Priority: P1)
**Goal**: All start surfaces create an `operation_runs` record at dispatch time; no job fallback-create; “View run” link is stable.
**Independent Test**: Start each in-scope operation and assert the `operation_runs` row exists before work begins, with correct type/identity/context and a stable tenantless view URL.
### Tests (US1)
- [ ] T008 [P] [US1] Add/extend tests for OperationRun dispatch-time creation in `tests/Feature/OperationRunServiceTest.php`
- [ ] T009 [P] [US1] Add/extend tests for start-surface authorization (403 prevents run creation) in `tests/Feature/RunStartAuthorizationTest.php`
### Implementation (US1)
- [ ] T010 [US1] Ensure inventory sync start surface creates OperationRun before dispatch and uses canonical link in `app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php`
- [ ] T011 [US1] Ensure directory groups sync start surface creates OperationRun before dispatch and uses canonical link in `app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php`
- [ ] T012 [US1] Ensure backup schedule manual run-now creates OperationRun before dispatch with unique-per-click identity (nonce) in `app/Filament/Resources/BackupScheduleResource.php`
- [ ] T013 [US1] Ensure backup schedule retry creates OperationRun before dispatch with unique-per-click identity (nonce) in `app/Filament/Resources/BackupScheduleResource.php`
- [ ] T014 [US1] Ensure scheduled backup dispatcher creates OperationRun before dispatch with strict identity by (schedule_id, scheduled_for) and type `backup_schedule.scheduled` in `app/Services/BackupScheduling/BackupScheduleDispatcher.php`
- [ ] T014a [US1] Enforce strict scheduled backup idempotency (at most one canonical run ever per schedule_id + intended fire-time), using an explicit DB constraint and/or lock strategy aligned with `OperationRunService` identities
- [ ] T015 [US1] Enforce “no job fallback-create” by validating required OperationRun context is present; fail fast if missing in `app/Jobs/RunInventorySyncJob.php`, `app/Jobs/EntraGroupSyncJob.php`, and `app/Jobs/RunBackupScheduleJob.php`
### Restore (US1)
- [ ] T015a [P] [US1] Add/extend tests that starting a restore execution creates an OperationRun at dispatch time (target existing restore execution tests under `tests/Feature/RestoreRunWizardExecuteTest.php` and/or `tests/Feature/ExecuteRestoreRunJobTest.php`)
- [ ] T015b [US1] Ensure the restore execution start surface creates OperationRun before dispatch and surfaces the stable canonical “View run” link (adjust the Filament restore execution action/page used in the wizard flow)
- [ ] T015c [US2] Ensure restore domain records link to canonical OperationRuns for observability (align with FR-014; no legacy fallback-create)
**Checkpoint**: Starting operations always yields a stable `/admin/operations/{run}` link immediately.
---
## Phase 4: User Story 2 — Monitor executions from a single canonical viewer (Priority: P2)
**Goal**: Canonical viewer and Monitoring pages remain DB-only; legacy run history pages are read-only and redirect only when a deterministic mapping exists.
**Independent Test**: Load canonical viewer and legacy view pages while asserting no outbound Graph calls occur during render/search/label callbacks.
### Tests (US2)
- [ ] T016 [P] [US2] Add tests asserting Monitoring pages render DB-only (no Graph calls) in `tests/Feature/Monitoring/MonitoringOperationsTest.php`
- [ ] T017 [P] [US2] Add tests for legacy-to-canonical redirect when mapping exists and no redirect when mapping absent in `tests/Feature/Operations/` (new file: `tests/Feature/Operations/LegacyRunRedirectTest.php`)
### Implementation (US2)
- [ ] T018 [US2] Add nullable `operation_run_id` mapping column + FK/index to legacy table `inventory_sync_runs` (new migration in `database/migrations/**_add_operation_run_id_to_inventory_sync_runs_table.php`)
- [ ] T019 [US2] Add nullable `operation_run_id` mapping column + FK/index to legacy table `entra_group_sync_runs` (new migration in `database/migrations/**_add_operation_run_id_to_entra_group_sync_runs_table.php`)
- [ ] T020 [US2] Add nullable `operation_run_id` mapping column + FK/index to legacy table `backup_schedule_runs` (new migration in `database/migrations/**_add_operation_run_id_to_backup_schedule_runs_table.php`)
- [ ] T021 [US2] Stop writing NEW legacy run rows for inventory sync and use `operation_runs` only for execution tracking (adjust service + callers in `app/Services/Inventory/InventorySyncService.php` and any start surfaces)
- [ ] T022 [US2] Stop writing NEW legacy run rows for directory group sync and use `operation_runs` only for execution tracking (adjust service + callers in `app/Services/Directory/EntraGroupSyncService.php` and any start surfaces)
- [ ] T023 [US2] Stop writing NEW legacy run rows for backup schedule executions and use `operation_runs` only for execution tracking; keep legacy table strictly read-only history for existing rows (adjust dispatcher and UI surfaces in `app/Services/BackupScheduling/BackupScheduleDispatcher.php` and `app/Filament/Resources/BackupScheduleResource.php`)
- [ ] T023a [US2] Update Backup Schedule UI to show new executions from `operation_runs` (query by type + context like schedule_id) and link to canonical viewer; legacy runs list remains history-only
- [ ] T024 [US2] Implement deterministic redirect on legacy “view” pages when `operation_run_id` exists in `app/Filament/Resources/InventorySyncRunResource/Pages/ViewInventorySyncRun.php` and `app/Filament/Resources/EntraGroupSyncRunResource/Pages/ViewEntraGroupSyncRun.php`
- [ ] T025 [US2] Ensure legacy run history pages remain strictly read-only (remove/disable start/retry actions) in `app/Filament/Resources/InventorySyncRunResource.php`, `app/Filament/Resources/EntraGroupSyncRunResource.php`, and `app/Filament/Resources/BackupScheduleResource/RelationManagers/BackupScheduleRunsRelationManager.php`
**Checkpoint**: Canonical viewer is the only execution-tracker UI; legacy is view-only and redirects only when mapped.
---
## Phase 5: User Story 3 — Use cached directory data in forms without blocking calls (Priority: P3)
**Goal**: Tenant configuration selectors use cached directory groups + cached role definitions; “Sync now” triggers async sync with an immediate canonical run link; no outbound calls during render/search/label callbacks.
**Independent Test**: Render Tenant configuration forms and exercise search/label callbacks while asserting Graph client is not called.
### Tests (US3)
- [ ] T026 [P] [US3] Add tests that TenantResource role definition selectors render/search DB-only (no Graph calls) in `tests/Feature/Filament/` (new file: `tests/Feature/Filament/TenantRoleDefinitionsSelectorDbOnlyTest.php`)
- [ ] T027 [P] [US3] Add tests that “Sync now” creates an OperationRun and returns a canonical view link in `tests/Feature/DirectoryGroups/` or `tests/Feature/TenantRBAC/` (choose closest existing folder)
- [ ] T027a [P] [US3] Add tests that directory group selectors render/search DB-only (no Graph calls) and use cached DB tables (new file under `tests/Feature/DirectoryGroups/` or `tests/Feature/Filament/`)
### Implementation (US3)
- [ ] T028 [US3] Create cached role definitions table + model + factory (new migration in `database/migrations/**_create_entra_role_definitions_table.php`, model in `app/Models/EntraRoleDefinition.php`, factory in `database/factories/EntraRoleDefinitionFactory.php`)
- [ ] T029 [US3] Add “role definitions sync” operation type `directory_role_definitions.sync` to `app/Support/OperationRunType.php` and label in `app/Support/OperationCatalog.php` (if not already completed in T005/T006)
- [ ] T030 [US3] Implement role definitions sync service + job that updates the cache and records progress/failures on the OperationRun (service in `app/Services/Directory/RoleDefinitionsSyncService.php`, job in `app/Jobs/SyncRoleDefinitionsJob.php`)
- [ ] T030a [US3] Register/verify Graph contract entries required for role definitions sync in `config/graph_contracts.php` and ensure the sync uses `GraphClientInterface` only (no ad-hoc endpoints)
- [ ] T031 [US3] Update `app/Filament/Resources/TenantResource.php` roleDefinitions search/label callbacks to query cached DB tables only (remove Graph calls from callbacks)
- [ ] T032 [US3] Add a non-destructive “Sync now” Filament action that dispatches `directory_role_definitions.sync` and provides a canonical “View run” link (in `app/Filament/Resources/TenantResource.php`)
**Checkpoint**: Tenant configuration selectors are DB-only; cache sync is async and observable via canonical run.
---
## Phase 6: Polish & Cross-Cutting Concerns
- [ ] T033 Ensure new/modified destructive-like actions (if any) use `Action::make(...)->action(...)->requiresConfirmation()` and are authorized server-side (audit existing touched Filament actions under `app/Filament/**`)
- [ ] T034 Run Pint on changed files via `vendor/bin/sail bin pint --dirty`
- [ ] T035 Run targeted test subset per quickstart: `vendor/bin/sail artisan test --compact --filter=OperationRun` and the new/changed test files
---
## Dependencies & Execution Order
### Phase Dependencies
- Setup (Phase 1) → Foundational (Phase 2) → US1 (Phase 3) → US2 (Phase 4) → US3 (Phase 5) → Polish (Phase 6)
### User Story Dependencies
- US1 is the MVP: it enables stable canonical run creation + links.
- US2 depends on Foundational + US1 (viewer/auth semantics), but can be implemented in parallel once viewer auth is stable.
- US3 depends on Foundational + cache primitives, but can proceed after Foundational even if US2 is in progress.
### Parallel Execution Examples
- US1 parallelizable: T008 + T009 (tests) can be written in parallel; start-surface patches T010T014 can be split across different files.
- US2 parallelizable: migrations T018T020 can be done in parallel; legacy resource updates T024T025 can be split by resource.
- US3 parallelizable: schema/model/factory T028 can be done while tests T026T027 are being drafted.