# Implementation Plan: Remove Legacy BulkOperationRun & Canonicalize Operations (v1.0) **Branch**: `056-remove-legacy-bulkops` | **Date**: 2026-01-18 | **Spec**: [specs/056-remove-legacy-bulkops/spec.md](./spec.md) **Input**: Feature specification from `/specs/056-remove-legacy-bulkops/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Unify all bulk/operational work onto `OperationRun` (canonical run model + Monitoring surface) by migrating all legacy `BulkOperationRun` workflows to OperationRun-backed orchestration, removing the legacy stack (model/service/table/UI), and adding guardrails that prevent reintroduction. ## Technical Context **Language/Version**: PHP 8.4.x **Primary Dependencies**: Laravel 12, Filament v4, Livewire v3 **Storage**: PostgreSQL (JSONB in `operation_runs.context`, `operation_runs.summary_counts`) **Testing**: Pest (PHPUnit 12) **Target Platform**: Docker via Laravel Sail (local); Dokploy (staging/prod) **Project Type**: Web application (Laravel monolith) **Performance Goals**: Calm polling UX for Monitoring; bulk orchestration chunked and resilient to throttling; per-scope concurrency default=1 **Constraints**: Tenant isolation; no secrets in run failures/notifications; no remote work during UI render; Monitoring is DB-only **Scale/Scope**: Bulk actions may target large selections; orchestration must remain idempotent and debounced by run identity ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first: N/A (this feature is operations plumbing; no inventory semantics change) - Read/write separation: PASS (bulk actions remain enqueue-only; write paths are job-backed and auditable) - Graph contract path: PASS (no new render-side Graph calls; any remote work stays behind queue + existing Graph client boundary) - Deterministic capabilities: PASS (no capability derivation changes) - Tenant isolation: PASS (OperationRun is tenant-scoped; bulk dedupe is tenant-wide; selection identity is deterministic) - Run observability: PASS (bulk is always OperationRun-backed; DB-only <2s actions remain audit-only) - Automation: PASS (locks + idempotency required; per-target concurrency is config-driven default=1) - Data minimization: PASS (failure summaries stay sanitized; no secrets in run records) ## Project Structure ### Documentation (this feature) ```text specs/056-remove-legacy-bulkops/ ├── plan.md # This file (/speckit.plan command output) ├── research.md # Phase 0 output (/speckit.plan command) ├── data-model.md # Phase 1 output (/speckit.plan command) ├── quickstart.md # Phase 1 output (/speckit.plan command) ├── contracts/ # Phase 1 output (/speckit.plan command) └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ ├── Resources/ │ └── Pages/ ├── Jobs/ ├── Models/ ├── Notifications/ ├── Services/ └── Support/ config/ ├── tenantpilot.php └── graph_contracts.php database/ ├── migrations/ └── factories/ resources/ └── views/ routes/ └── web.php tests/ ├── Feature/ └── Unit/ ``` **Structure Decision**: Laravel web application (monolith). Feature changes are expected primarily under `app/` (runs, jobs, Filament actions), `database/migrations/` (dropping legacy tables), and `tests/` (Pest guardrails). ## Complexity Tracking > **Fill ONLY if Constitution Check has violations that must be justified** | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | None | N/A | N/A | ## Constitution Check (Post-Design) *Re-check after Phase 1 outputs are complete.* - Monitoring remains DB-only at render time. - Start surfaces remain enqueue-only. - Bulk work is always OperationRun-backed. - Per-target scope concurrency is config-driven (default=1). - Bulk idempotency uses hybrid selection identity. ## Phase 0 — Outline & Research (output: research.md) ### Discovery signals (repo sweep) Legacy bulk-run usage exists and must be migrated/removed: - Jobs using legacy run/service include: - `app/Jobs/BulkPolicySyncJob.php` - `app/Jobs/BulkPolicyDeleteJob.php` - `app/Jobs/BulkBackupSetDeleteJob.php` - `app/Jobs/BulkPolicyVersionPruneJob.php` - `app/Jobs/BulkPolicyVersionForceDeleteJob.php` - `app/Jobs/BulkRestoreRunDeleteJob.php` - `app/Jobs/BulkTenantSyncJob.php` - `app/Jobs/CapturePolicySnapshotJob.php` - `app/Jobs/GenerateDriftFindingsJob.php` (mixed: legacy + OperationRun) Legacy data layer present: - Model: `app/Models/BulkOperationRun.php` - Service: `app/Services/BulkOperationService.php` - Migrations: `database/migrations/*_create_bulk_operation_runs_table.php`, `*_add_idempotency_key_to_bulk_operation_runs_table.php`, `*_increase_bulk_operation_runs_status_length.php` - Factory/Seeder: `database/factories/BulkOperationRunFactory.php`, `database/seeders/BulkOperationsTestSeeder.php` Existing canonical run patterns to reuse: - `app/Services/OperationRunService.php` provides tenant-wide active-run dedupe and safe dispatch semantics. - `app/Support/OperationCatalog.php` centralizes labels/durations and allowed summary keys. - `app/Support/OpsUx/OperationSummaryKeys.php` + `SummaryCountsNormalizer` enforce summary_counts contract. Concurrency/idempotency patterns already exist and should be adapted: - `app/Services/Inventory/InventoryConcurrencyLimiter.php` uses per-scope cache locks with config-driven defaults. - `app/Services/Inventory/InventorySyncService.php` uses selection hashing + selection locks to prevent duplicate work. ### Research outputs (decisions) - Per-target scope concurrency is configuration-driven with default=1. - Bulk selection identity is hybrid (IDs-hash when explicit IDs; query-hash when “select all via filter/query”). - Legacy bulk-run history is not imported into OperationRun (default path). ## Phase 1 — Design & Contracts (outputs: data-model.md, contracts/*, quickstart.md) ### Design topics - Define the canonical bulk orchestration shape around OperationRun (orchestrator + workers) while preserving the constitution feedback surfaces. - Define the minimal run context contract for directory-targeted runs (target scope fields + idempotency fingerprint fields). - Extend operation type catalog for newly migrated bulk operations. ### Contract artifacts - JSON schema for `operation_runs.context` for bulk operations (target scope + selection identity + idempotency). - OpenAPI sketch for internal operation-triggering endpoints (if applicable) as a stable contract for “enqueue-only” start surfaces. ## Phase 2 — Planning (implementation outline; detailed tasks live in tasks.md) - Perform required discovery sweep and create a migration report. - Migrate each legacy bulk workflow to OperationRun-backed orchestration. - Remove legacy model/service/table/UI surfaces. - Add hard guardrails (CI/test) to forbid legacy references and verify run-backed behavior. - Add targeted Pest tests for bulk actions, per-scope throttling default=1, and summary key normalization.