# Phase 0 Research: Ops-UX Enforcement & Cleanup This research document resolves implementation uncertainties for the enforcement spec and records key decisions. ## Decision 1 — Implement guards as Pest “architecture tests” (filesystem scan) **Decision**: Implement Guard A/B/C as Pest tests that scan PHP source files in `app/` (and for Guard C also `tests/`) and fail CI with actionable file + snippet output. **Rationale**: - Runs in the same CI pipeline as the rest of the test suite (no new tooling step). - Enforcement is repo-wide and does not rely on developer discipline or code review memory. - Can produce highly actionable failure output (file path + snippet) without external dependencies. **Alternatives considered**: - PHPStan custom rules: higher precision, but adds configuration + toolchain scope. - `nikic/php-parser` AST scanning: high precision but adds a dependency (out of bounds per repo constraints). ## Decision 2 — Guard A detection uses tokenizer-assisted scanning (not pure regex) **Decision**: Implement Guard A using PHP’s built-in tokenizer (`token_get_all()`) to detect `->update([...])` call sites and then inspect the array literal for `status` / `outcome` keys. **Rationale**: - Pure multi-line regex is brittle with nested arrays, comments, and strings. - Tokenization gives a dependency-free way to avoid common false positives and to compute an approximate line number. - The spec’s intended heuristic (“`->update([...])` block contains status/outcome keys”) maps naturally to tokens. **Alternatives considered**: - Regex-only `DOTALL` matching with string/comment skipping (`(*SKIP)(*F)`): simpler, but can still drift across the wrong closing bracket and become noisy. - Manual char-by-char scanner after a simple “find start” regex: workable, but token-based is typically easier to maintain in PHP. **Repo evidence**: - Canonical status/outcome transitions live in `OperationRunService::updateRun()`. - Violations exist in jobs/services that call `$operationRun->update([... 'status' => ..., 'outcome' => ...])` directly (enumerated in the spec). ## Decision 3 — Guard B flags “OperationRun in scope” + “DB notification emission” **Decision**: Implement Guard B as a two-signal scan: - **OperationRun signal**: file contains any of: `use App\\Models\\OperationRun;`, `OperationRun`, `$this->operationRun`, `$operationRun`. - **DB emission signal**: file contains either `sendToDatabase(` or `->notify(`. - **Allowlist**: only `app/Services/OperationRunService.php` and `app/Notifications/OperationRunCompleted.php`. **Rationale**: - Enforces the constitution: completion notifications are centralized, queued/running DB notifications are banned. - Intentionally errs on the side of flagging new ad-hoc notification producers. **Alternatives considered**: - “DB emission only” = `sendToDatabase(` and ignore `->notify(`: lower noise, but would miss notify-based DB notifications. - Restrict scan to `app/Jobs/**` and `app/Services/**`: lower noise, but spec requires `app/**/*.php` for defense-in-depth. **Repo evidence (high-level)**: - `OperationRunService` currently emits both `OperationRunCompleted` and `OperationRunQueued` via `->notify(...)`. - There are multiple job files with `->sendToDatabase(...)` in code paths that also handle an OperationRun. - There are Filament resource actions that both dispatch jobs with `$operationRun` and call `->sendToDatabase(...)` for queued feedback. These are queued DB notifications and conflict with FR-004. ## Decision 4 — Ban queued DB notifications by changing service defaults **Decision**: Change `OperationRunService::dispatchOrFail(..., bool $emitQueuedNotification = true)` (and any upstream helper that takes `emitQueuedNotification`) so the default is `false`, and ensure no call site opts back into queued DB notifications. **Rationale**: - Matches FR-004 / FR-012b: queued/running DB notifications are forbidden repo-wide. - Prevents new call sites from accidentally enabling queued DB notifications by omission. **Alternatives considered**: - Delete `OperationRunQueued` entirely: higher churn; leaving the class unused is acceptable as long as it’s never emitted. - Keep default `true` but require opt-out everywhere: violates FR-012b and is easy to regress. ## Decision 5 — “Exactly once terminal notification” relies on `previousStatus` guard **Decision**: Rely on `OperationRunService::updateRun()` behavior that emits `OperationRunCompleted` only when transitioning into `Completed` from a non-completed status. **Rationale**: - Centralizes “exactly once” semantics in a single place. - Keeps jobs/services free of notification-specific logic. **Alternatives considered**: - Add additional DB-level dedupe: unnecessary if the service is the only producer and transitions are canonical.