# Implementation Plan: IA Semantics — Scope vs Filter vs Targeting (Monitoring + Manage) **Branch**: `103-ia-scope-filter-semantics` | **Date**: 2026-02-20 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `/specs/103-ia-scope-filter-semantics/spec.md` ## Summary Replace "Scope: …" wording with "Filtered by tenant: …" / "All tenants" on Monitoring canonical pages, fix the `AlertsKpiHeader` tenant-resolution bug that causes KPI numbers to diverge from the indicator when lastTenantId fallback is active, remove the semantically incorrect tenant indicator from workspace-owned Manage pages (Alert Rules, Alert Destinations), and relabel + restructure the AlertRule edit form to use targeting semantics instead of scope semantics. **Technical approach**: Update `OperateHubShell::scopeLabel()` (single source of truth for indicator copy), fix `AlertsKpiHeader::deliveriesQueryForViewer()` to call `OperateHubShell::activeEntitledTenant()` instead of `Filament::getTenant()`, remove header-action spreads from Manage list pages, and refactor `AlertRuleResource::form()` labels/sections. ## Technical Context **Language/Version**: PHP 8.4 (Laravel 12) **Primary Dependencies**: Filament v5, Livewire v4, `OperateHubShell` support class **Storage**: PostgreSQL — no schema changes **Testing**: Pest v4 (feature tests) **Target Platform**: Web (Laravel Sail / Dokploy) **Project Type**: Web application (Laravel monolith) **Performance Goals**: N/A — copy + bugfix, no performance-sensitive changes **Constraints**: DB-only renders on Monitoring pages (no Graph/HTTP calls) **Scale/Scope**: 5 source files modified, 1–2 test files updated/created ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - [x] **Inventory-first**: N/A — no inventory data touched. - [x] **Read/write separation**: No new writes introduced. The KPI bugfix corrects an existing read query filter — no preview/confirmation/audit needed. - [x] **Graph contract path**: No Graph calls introduced or modified. All changes are DB-only. - [x] **Deterministic capabilities**: No capability changes. Existing RBAC enforcement unchanged. - [x] **RBAC-UX (two planes)**: No routing changes. All affected pages remain on `/admin` plane. No cross-plane routing introduced. - [x] **Workspace isolation**: No workspace isolation changes. Monitoring pages remain workspace-context. Manage pages remain workspace-owned. - [x] **RBAC-UX (destructive actions)**: No destructive actions added. Removal of OperateHubShell header actions from Manage pages removes informational indicators, not mutation actions. - [x] **RBAC-UX (global search)**: No global search changes. - [x] **Tenant isolation**: The bugfix *improves* tenant isolation by ensuring KPI queries respect the resolved tenant context. No cross-tenant views introduced. - [x] **Run observability**: No long-running/remote/queued work introduced. All changes are synchronous UI/copy. - [x] **Automation**: N/A — no queued/scheduled ops. - [x] **Data minimization**: N/A — no new data storage. - [x] **Badge semantics (BADGE-001)**: No status badges added or changed. The "Filtered by tenant" indicator is informational copy, not a status badge. - [x] **Filament UI Action Surface Contract**: See UI Action Matrix in spec.md. No new actions. Removed header actions on Manage pages were informational. Existing destructive actions unchanged. - [x] **Filament UI UX-001**: AlertRule form adds Section grouping (conforming to "all fields inside Sections/Cards"). No new pages. **Post-Phase 1 re-check**: All gates remain satisfied. No design decisions introduced violations. ## Project Structure ### Documentation (this feature) ```text specs/103-ia-scope-filter-semantics/ ├── plan.md # This file ├── spec.md # Feature specification ├── research.md # Phase 0 output — codebase research findings ├── data-model.md # Phase 1 output — no schema changes documented ├── quickstart.md # Phase 1 output — files to touch + verification commands ├── checklists/ │ └── requirements.md # Spec quality checklist (from /speckit.spec) └── tasks.md # Phase 2 output (NOT created by /speckit.plan) ``` ### Source Code (files to modify) ```text app/ ├── Support/OperateHub/ │ └── OperateHubShell.php # T1: Update scopeLabel() copy ├── Filament/ │ ├── Widgets/Alerts/ │ │ └── AlertsKpiHeader.php # T2: Fix deliveriesQueryForViewer() bug │ ├── Resources/ │ │ ├── AlertRuleResource.php # T4/T5: Relabel form + add Sections │ │ └── AlertRuleResource/Pages/ │ │ └── ListAlertRules.php # T3: Remove OperateHubShell header spread │ └── Resources/AlertDestinationResource/Pages/ │ └── ListAlertDestinations.php # T3: Remove OperateHubShell header spread tests/ ├── Feature/OpsUx/ │ └── OperateHubShellTest.php # T6: Update existing assertions for new copy └── Feature/Filament/Alerts/ └── AlertRuleCrudTest.php # T6: Verify/update label-dependent assertions ``` **Structure Decision**: Existing Laravel monolith structure. All modifications are to existing files. No new directories or structural changes. ## Implementation Tasks ### T1 — Update `OperateHubShell::scopeLabel()` Copy (FR-001, FR-002, FR-003) **File**: `app/Support/OperateHub/OperateHubShell.php` (line 23–33) **Current**: ```php public function scopeLabel(?Request $request = null): string { $activeTenant = $this->activeEntitledTenant($request); if ($activeTenant instanceof Tenant) { return 'Scope: Tenant — '.$activeTenant->name; } return 'Scope: Workspace — all tenants'; } ``` **Target**: ```php public function scopeLabel(?Request $request = null): string { $activeTenant = $this->activeEntitledTenant($request); if ($activeTenant instanceof Tenant) { return 'Filtered by tenant: '.$activeTenant->name; } return 'All tenants'; } ``` **Propagation**: This single change propagates to all consumers: - `headerActions()` within `OperateHubShell` itself (L61) - `Operations.php` (L66) — uses `$operateHubShell->scopeLabel(request())` directly - `TenantlessOperationRunViewer.php` (L47) — uses `$operateHubShell->scopeLabel(request())` directly - `AuditLog.php`, `Alerts.php`, `ListAlertDeliveries.php` — via `headerActions()` **FR mapping**: FR-001 (filtered indicator), FR-002 (all-tenants indicator), FR-003 (no "Scope:" text), FR-014 (DB-only renders unchanged). --- ### T2 — Fix `AlertsKpiHeader::deliveriesQueryForViewer()` Bug (FR-004, FR-005) **File**: `app/Filament/Widgets/Alerts/AlertsKpiHeader.php` (line 101–118) **Current** (buggy): ```php protected function deliveriesQueryForViewer(): Builder { $query = AlertDelivery::query()->where(...); $tenant = Filament::getTenant(); // ← BUG: null when lastTenantId fallback if ($tenant) { $query->where('tenant_id', $tenant->id); } return $query; } ``` **Target**: ```php protected function deliveriesQueryForViewer(): Builder { $query = AlertDelivery::query()->where(...); $tenant = app(OperateHubShell::class)->activeEntitledTenant(request()); if ($tenant instanceof Tenant) { $query->where('tenant_id', $tenant->id); } return $query; } ``` **FR mapping**: FR-004 (consistent tenant resolution), FR-005 (KPI filters by resolved tenant). --- ### T3 — Remove OperateHubShell Header Actions from Manage Pages (FR-006) **Files**: - `app/Filament/Resources/AlertRuleResource/Pages/ListAlertRules.php` (L23–37) - `app/Filament/Resources/AlertDestinationResource/Pages/ListAlertDestinations.php` (L17–30) **Change**: Remove `...app(OperateHubShell::class)->headerActions(...)` from `getHeaderActions()`. Keep only the `CreateAction::make()`. **FR mapping**: FR-006 (no tenant indicator on Manage pages). --- ### T4 — Relabel AlertRule Form Fields (FR-007, FR-008, FR-009, FR-010) **File**: `app/Filament/Resources/AlertRuleResource.php` (form method, ~L155+) **Changes**: | Current | Target | |---------|--------| | `Select::make('tenant_scope_mode')` label (auto-generated "Tenant scope mode") | Explicit `->label('Applies to tenants')` | | Option `'Allowlist'` | `'Selected tenants'` | | `Select::make('tenant_allowlist')->label('Tenant allowlist')` | `->label('Selected tenants')` | | No helper texts | `->helperText('This rule is workspace-wide. Use this to limit where it applies.')` on tenant_scope_mode | | No helper texts | `->helperText('Only these tenants will trigger this rule.')` on tenant_allowlist | **DB values**: `all` / `allowlist` constants UNCHANGED. Only display labels change. **FR mapping**: FR-007, FR-008, FR-009, FR-010, FR-012 (no domain behavior changes), FR-013 (no schema changes). --- ### T5 — Add Section Grouping to AlertRule Form (FR-011) **File**: `app/Filament/Resources/AlertRuleResource.php` (form method) **Change**: Wrap existing flat fields into three `Filament\Schemas\Components\Section` groups: 1. **"Rule"** section: Name, Enabled, Event type, Minimum severity 2. **"Applies to"** section: Applies to tenants (tenant_scope_mode), Selected tenants (tenant_allowlist, conditional) 3. **"Delivery"** section: Cooldown, Quiet hours, Destinations **Import**: `use Filament\Schemas\Components\Section;` (verified in 11 existing files). **FR mapping**: FR-011. --- ### T6 — Update Tests (all FRs) **Files to update**: - `tests/Feature/OpsUx/OperateHubShellTest.php` — update all `assertSee('Scope: Workspace — all tenants')` → `assertSee('All tenants')` and all `assertSee('Scope: Tenant — ')` → `assertSee('Filtered by tenant: ')` - `tests/Feature/Filament/Alerts/AlertRuleCrudTest.php` — check for label-dependent assertions, update if needed **New test coverage** (add to existing test files or create new): - KPI consistency: Set tenant-context via lastTenantId only → assert `deliveriesQueryForViewer()` includes `where tenant_id = X` - Manage pages: Visit `/admin/alert-rules` with tenant-context → `assertDontSee('Filtered by tenant')` and `assertDontSee('Scope: Tenant')` - Form labels: Visit AlertRule edit page → `assertSee('Applies to tenants')`, `assertDontSee('Tenant scope mode')` - Section headings: Visit AlertRule edit page → `assertSee('Rule')`, `assertSee('Applies to')`, `assertSee('Delivery')` --- ### T7 — (Optional, P3) "Add Current Tenant" Convenience Button (FR: User Story 6) **File**: `app/Filament/Resources/AlertRuleResource.php` **Change**: Add a form action button visible when tenant-context is active AND "Selected tenants" mode is visible. On click, append current tenant to `tenant_allowlist` field state. **Note**: Only implement if T1–T6 are complete and PR remains small. Skip if scope exceeds budget. ## Filament v5 Compliance (Agent Output Contract) 1. **Livewire v4.0+ compliance**: All code targets Livewire v4.0+. No v3 references. 2. **Provider registration**: No new providers. Existing panel provider in `bootstrap/providers.php` unchanged. 3. **Globally searchable resources**: AlertRuleResource has `$isScopedToTenant = false` — global search not applicable to workspace-owned resources. No changes to global search behavior. 4. **Destructive actions**: No destructive actions added or modified. Existing destructive actions on these pages retain `->requiresConfirmation()`. 5. **Asset strategy**: No new assets registered. No changes to `filament:assets` deployment. 6. **Testing plan**: OperateHubShellTest updated for new copy. AlertRuleCrudTest verified/updated. New test assertions for KPI consistency, Manage page indicator suppression, and form labels/sections. ## Complexity Tracking > No constitution violations. No complexity justifications needed.