# Implementation Plan: Workspace Baseline Compare Matrix V1 **Branch**: `190-baseline-compare-matrix` | **Date**: 2026-04-11 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/190-baseline-compare-matrix/spec.md` **Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/190-baseline-compare-matrix/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Add one workspace-scoped Filament page anchored on `BaselineProfile` that renders a tenant-by-subject compare matrix from the current effective baseline snapshot, visible assigned tenants, the latest relevant tenant `baseline_compare` runs, and compare-created findings. Reuse the existing tenant compare start path for `Compare assigned tenants`, reuse `CanonicalNavigationContext` for drilldown continuity, and keep V1 derived and read-only so no new persisted cross-tenant compare artifact or workspace umbrella run is introduced. ## Technical Context **Language/Version**: PHP 8.4.15 **Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns **Storage**: PostgreSQL via existing `baseline_profiles`, `baseline_snapshots`, `baseline_snapshot_items`, `baseline_tenant_assignments`, `operation_runs`, and `findings` tables; no new persistence planned **Testing**: Pest 4 feature and browser tests, Filament or Livewire page coverage, focused RBAC regressions, run through Laravel Sail **Target Platform**: Laravel monolith web application in Sail locally and containerized Linux deployment in staging and production **Project Type**: web application **Performance Goals**: Keep matrix reads query-bounded over the visible tenant set and reference snapshot, avoid per-cell N+1 queries, keep render-time surfaces DB-only, and keep `Compare assigned tenants` enqueue-only by reusing tenant compare run starts **Constraints**: No new persisted cross-tenant compare artifact; no new workspace umbrella `OperationRun`; hidden tenants must not leak by name or count; `Compare assigned tenants` must remain confirmation-gated and `simulation only`; matrix uses a narrow custom grid exception because a one-axis table cannot represent subject-by-tenant truth cleanly; no remote calls at render time **Scale/Scope**: One workspace, one baseline profile at a time, all visible assigned tenants for that profile, all in-scope baseline subjects for the effective snapshot, one new page and view, one narrow derived matrix builder, additive baseline-detail header actions, and focused feature plus smoke-test coverage ## Constitution Check *GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.* | Principle | Pre-Research | Post-Design | Notes | |-----------|--------------|-------------|-------| | Inventory-first / snapshots-second | PASS | PASS | The matrix reads current tenant evidence through existing compare runs and baseline snapshots; it does not redefine last-observed truth. | | Read/write separation | PASS | PASS | The feature is read-only apart from compare start. `Compare assigned tenants` remains confirmation-gated and simulation-only, with existing run and audit semantics. | | Graph contract path | N/A | N/A | No new Graph calls or contract-registry changes are required. | | Deterministic capabilities | PASS | PASS | Existing capability constants `WORKSPACE_BASELINES_VIEW`, `WORKSPACE_BASELINES_MANAGE`, `TENANT_VIEW`, and `TENANT_FINDINGS_VIEW` remain canonical. | | Workspace + tenant isolation | PASS | PASS | The matrix is workspace-scoped, its columns are limited to visible assigned tenants, and all drilldowns re-check tenant entitlement. | | RBAC-UX authorization semantics | PASS | PASS | Non-members remain `404`; in-scope members missing required capability remain `403`; server-side authorization remains authoritative. | | Run observability / Ops-UX | PASS | PASS | `Compare assigned tenants` fans out to existing tenant `baseline_compare` runs only. No new shadow run model or inline remote work is introduced. | | Data minimization | PASS | PASS | The matrix reads existing compare and finding metadata only; no new payload copies or exported artifacts are stored. | | Proportionality / no premature abstraction | PASS | PASS | The design adds one narrow matrix builder and one new page instead of a generalized compare framework or persisted portfolio layer. | | Persisted truth / behavioral state | PASS | PASS | No new persistence or state family is added. Matrix states remain derived from existing truth. | | UI semantics / few layers | PASS | PASS | Trust, stale, and ambiguity reuse existing compare semantics and do not create a new UI taxonomy. | | Filament v5 / Livewire v4 compliance | PASS | PASS | The feature stays inside current Filament v5 and Livewire v4 patterns. | | Provider registration location | PASS | PASS | No panel or provider changes are required. Provider registration remains in `bootstrap/providers.php`. | | Global search hard rule | PASS | PASS | No new globally searchable resource is added. Existing baseline resources already satisfy current search constraints. | | Destructive action safety | PASS | PASS | No destructive action is added. Existing destructive baseline actions remain unchanged and confirmation-gated. | | Asset strategy | PASS | PASS | No new assets are planned. Existing deployment behavior for `filament:assets` remains unchanged. | | Filament-native UI / Action Surface Contract | PASS WITH NARROW EXCEPTION | PASS WITH NARROW EXCEPTION | The matrix uses native Filament page structure and actions, but the core body is a custom two-dimensional grid because a one-axis table is insufficient. | | Filament UX-001 | PASS WITH NARROW EXCEPTION | PASS WITH NARROW EXCEPTION | Sections, filters, legends, and empty states remain standard. The matrix body itself is the documented UX-001 exception. | | Testing truth (TEST-TRUTH-001) | PASS | PASS | The test plan focuses on aggregation correctness, hidden-tenant safety, stale or ambiguous truth, compare-all fan-out, and drilldown continuity. | ## Phase 0 Research Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/190-baseline-compare-matrix/research.md`. Key decisions: - Use `/admin/baseline-profiles/{record}/compare-matrix` as the canonical route and anchor the workflow under the existing baseline profile resource. - Derive matrix cells and summaries from `BaselineSnapshotItem` plus the latest relevant `baseline_compare` run plus compare-created findings; findings alone are insufficient. - Treat technical deviation state as primary and finding workflow state as secondary metadata so closed or risk-accepted findings do not hide real current drift. - Reuse the existing stale-result threshold and current-reference comparison rather than inventing matrix-specific freshness rules. - Reuse existing evidence-gap and trustworthiness semantics for ambiguity and low-confidence signals. - Reuse `CanonicalNavigationContext` for matrix drilldown continuity instead of extending `PortfolioArrivalContext`. - Implement `Compare assigned tenants` as visible-tenant fan-out over existing tenant compare starts without a workspace umbrella run. - Validate primarily with focused feature tests plus one browser smoke test. ## Phase 1 Design Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/190-baseline-compare-matrix/`: - `data-model.md`: existing source truths, derived matrix read models, and cell-state precedence - `contracts/baseline-compare-matrix.logical.openapi.yaml`: internal logical contract for matrix reads and compare-all launch behavior - `quickstart.md`: implementation sequence and focused verification workflow Design decisions: - The matrix stays fully derived and introduces no new table, artifact, or workspace-level run type. - The new page is baseline-profile-scoped and reuses existing baseline detail, compare landing, finding detail, and operation-run viewer destinations. - The core builder lives alongside existing baseline support code and returns plain derived arrays or value-oriented payloads rather than a new presenter framework. - `Compare assigned tenants` iterates the visible assigned tenant set and reuses `BaselineCompareService::startCompare()` per tenant, reporting queued, already queued, and blocked results honestly. - Cell state precedence is fixed so `not_compared`, `stale_result`, `ambiguous`, `missing`, and `differ` remain visibly distinct from `match`. - Matrix drilldowns preserve a canonical return path by reusing `CanonicalNavigationContext` query propagation rather than inventing a new navigation token system. ## Project Structure ### Documentation (this feature) ```text specs/190-baseline-compare-matrix/ ├── spec.md ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── baseline-compare-matrix.logical.openapi.yaml ├── checklists/ │ └── requirements.md └── tasks.md ``` ### Source Code (repository root) ```text apps/platform/ ├── app/ │ ├── Filament/ │ │ ├── Pages/ │ │ │ ├── BaselineCompareLanding.php │ │ │ └── BaselineCompareMatrix.php │ │ └── Resources/ │ │ ├── BaselineProfileResource.php │ │ ├── BaselineProfileResource/ │ │ │ └── Pages/ViewBaselineProfile.php │ │ ├── FindingResource.php │ │ └── OperationRunResource.php │ ├── Models/ │ │ ├── BaselineProfile.php │ │ ├── BaselineSnapshot.php │ │ ├── BaselineSnapshotItem.php │ │ ├── BaselineTenantAssignment.php │ │ ├── Finding.php │ │ └── OperationRun.php │ ├── Services/ │ │ └── Baselines/BaselineCompareService.php │ ├── Support/ │ │ ├── Baselines/ │ │ │ ├── BaselineCompareEvidenceGapDetails.php │ │ │ ├── BaselineCompareMatrixBuilder.php │ │ │ ├── BaselineCompareStats.php │ │ │ ├── BaselineCompareSummaryAssessor.php │ │ │ └── BaselineSnapshotTruthResolver.php │ │ ├── Navigation/ │ │ │ ├── CanonicalNavigationContext.php │ │ │ └── RelatedNavigationResolver.php │ │ └── Rbac/ │ │ └── UiEnforcement.php │ └── resources/views/filament/pages/ │ └── baseline-compare-matrix.blade.php └── tests/ ├── Browser/ │ └── Spec190BaselineCompareMatrixSmokeTest.php ├── Feature/ │ ├── Baselines/ │ │ ├── BaselineCompareMatrixBuilderTest.php │ │ └── BaselineCompareMatrixCompareAllActionTest.php │ ├── Filament/ │ │ └── BaselineCompareMatrixPageTest.php │ └── Rbac/ │ └── BaselineCompareMatrixAuthorizationTest.php └── Feature/Guards/ └── ActionSurfaceContractTest.php ``` **Structure Decision**: Keep the work inside the existing Laravel monolith under `apps/platform`. Add one new workspace page, one new view, one narrow baseline-support matrix builder, and additive changes to existing baseline detail and drilldown surfaces instead of creating a new domain package, new persistence layer, or new top-level workspace app. ## Implementation Strategy ### Phase A — Add The Workspace Entry Surface **Goal**: Introduce the canonical matrix route and entry points without changing baseline ownership or navigation semantics. | Step | File | Change | |------|------|--------| | A.1 | `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` | Add the new workspace page class, scope it to one `BaselineProfile`, expose filter state, and declare the page contract plus action-surface intent | | A.2 | `apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php` | Add the page view using Filament sections, summaries, legends, and one narrow custom grid body | | A.3 | `apps/platform/app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php` | Add `Open compare matrix` and `Compare assigned tenants` header actions with confirmation and capability gating | ### Phase B — Build The Live Matrix Aggregation **Goal**: Derive visible rows, columns, cells, summaries, freshness, and trust from existing truth only. | Step | File | Change | |------|------|--------| | B.1 | `apps/platform/app/Support/Baselines/BaselineCompareMatrixBuilder.php` | Add the narrow read-model builder that resolves reference snapshot, visible assigned tenants, subject rows, matrix cells, and summaries | | B.2 | Existing baseline support classes such as `BaselineCompareStats.php`, `BaselineCompareEvidenceGapDetails.php`, and `BaselineCompareSummaryAssessor.php` | Reuse or extract helper logic for freshness, trust, evidence-gap interpretation, and no-result handling instead of duplicating semantics | | B.3 | Existing models `BaselineTenantAssignment`, `Finding`, and `OperationRun` | Add or reuse scoped queries for visible tenant assignments, latest relevant compare runs per tenant, and compare-created findings keyed to the selected baseline profile | ### Phase C — Add Compare-All Fan-Out Over Existing Tenant Runs **Goal**: Start compare for the visible assigned set without creating a workspace umbrella run. | Step | File | Change | |------|------|--------| | C.1 | `apps/platform/app/Services/Baselines/BaselineCompareService.php` | Add or extend a batch-start path that iterates visible assigned tenants and reuses `startCompare()` for each target | | C.2 | `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` | Surface `Compare assigned tenants`, call the batch-start path, render queued, already queued, and blocked outcomes honestly, and preserve visible-disabled helper text for in-scope users missing manage capability | | C.3 | Existing operation feedback helpers such as `OperationUxPresenter` and `OpsUxBrowserEvents` | Reuse current queued feedback and run-link behavior; do not invent matrix-specific run notifications | ### Phase D — Bind Filters, Drilldowns, And Degraded States **Goal**: Make the matrix operator-scanable, trustworthy, and continuous with existing follow-up surfaces. | Step | File | Change | |------|------|--------| | D.1 | `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` | Add policy-type, state, severity, tenant-sort, subject-sort, and focused-subject state | | D.2 | `apps/platform/app/Support/Navigation/CanonicalNavigationContext.php` and related call sites | Reuse canonical navigation context so cell, tenant, finding, and run drilldowns preserve a return path to the matrix | | D.3 | Existing drilldown destinations such as `BaselineCompareLanding.php`, `FindingResource.php`, and `OperationRunResource.php` | Accept bounded matrix source context and render a meaningful back link or source hint without adding a new navigation system | | D.4 | Matrix view and builder | Render explicit empty, blocked, partial, stale, ambiguous, and no-result states without collapsing them into matches | ### Phase E — Regression Protection And Hardening **Goal**: Prove business truth, hidden-tenant safety, and action-surface compliance. | Step | File | Change | |------|------|--------| | E.1 | `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php` | Cover row and cell derivation, stale or no-result logic, ambiguity, and visible-set-only summaries | | E.2 | `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php` | Cover compare-all fan-out, per-tenant queued or blocked outcomes, and no workspace umbrella run creation | | E.3 | `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php` | Cover page mount, filters, subject focus, degraded states, and drilldown link presence | | E.4 | `apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php` | Cover non-member `404`, member-without-capability `403`, visible-set filtering, and allowed access | | E.5 | `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php` | Add one smoke test for the rendered matrix, one core filter interaction, and one drilldown or compare-all affordance | | E.6 | `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php` | Keep the new page and baseline detail actions aligned with action-surface rules | ## Key Design Decisions ### D-001 — Keep the matrix fully derived The page must read from existing baseline, compare, finding, and run truth only. No `CrossTenantCompare`, no stored matrix snapshot, and no new reporting artifact are introduced in V1. ### D-002 — Keep baseline profile as the visible standard The matrix route lives under the selected baseline profile because the operator question is not generic cross-tenant compare; it is reference-to-many compare against one workspace-owned standard. ### D-003 — Reuse tenant compare runs only `Compare assigned tenants` fans out across the visible assigned tenant set through existing tenant compare starts. Aggregate progress on the matrix page derives from those tenant runs instead of a workspace batch run. ### D-004 — Technical drift comes before workflow posture Cells and counts reflect technical compare truth first. Finding workflow state, risk acceptance, or acknowledgement remain secondary metadata and do not suppress a current `differ` or `missing` state. ### D-005 — Reuse canonical navigation context Drilldown continuity uses existing canonical navigation patterns rather than extending portfolio-triage tokens or adding a new navigation framework. ### D-006 — Accept one narrow matrix-grid UI exception The matrix body is the only major UI exception because a standard table cannot represent subject-by-tenant truth. Everything around the grid remains Filament-native: actions, sections, legends, empty states, and badges. ## Risk Assessment | Risk | Impact | Likelihood | Mitigation | |------|--------|------------|------------| | Hidden-tenant leakage through summaries or totals | High | Medium | Build every column and summary from the visible tenant set only; add explicit negative RBAC tests. | | Findings-only aggregation understates current drift | High | Medium | Derive technical state from latest compare run plus findings; treat finding workflow as secondary metadata. | | Stale or no-result truth looks calm | High | Medium | Reuse stale-result semantics, explicit `not_compared`, and blocked-state rendering; test all degraded cases. | | Matrix render becomes query-heavy | High | Medium | Build one narrow aggregation layer that batches tenant runs, findings, and snapshot items instead of resolving per cell. | | Compare-all introduces a shadow batch model | Medium | Medium | Reuse `BaselineCompareService::startCompare()` per tenant and return a simple launch summary only. | | The page grows into a generalized standardization platform | Medium | Medium | Keep route, copy, and design anchored on one baseline profile, one reference snapshot, and read-only workflow scope. | ## Test Strategy - Add focused feature coverage for matrix aggregation, cell precedence, tenant and subject summaries, stale or no-result truth, and visible-set-only counts. - Add action coverage for compare-all fan-out, per-tenant queued or blocked outcomes, reuse of the existing tenant compare run path, service-owned run transitions, canonical summary-count invariants, and notification-surface limits. - Add RBAC coverage for workspace membership, capability denial, partial tenant visibility, visible-disabled helper-text states for in-scope users missing manage capability, and drilldown authorization semantics. - Add query-shape regression coverage so wide tenant/subject matrices stay batched and avoid per-cell N+1 resolution. - Add one browser smoke test for matrix rendering plus a core filter or drilldown interaction. - Keep action-surface and existing baseline compare suites green so the new page does not regress current baseline workflows. - Run the minimum focused Sail pack plus `pint --dirty --format agent` before implementation sign-off. ## Complexity Tracking | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | Two-dimensional matrix grid on a Filament page | The operator must scan subjects across multiple visible tenant columns in one surface | A standard Filament table or tenant-by-tenant list cannot represent both axes without destroying the core workflow question | ## Proportionality Review - **Current operator problem**: Operators can already define baselines and compare one tenant at a time, but they still cannot answer cross-tenant baseline deviation questions without manual aggregation. - **Existing structure is insufficient because**: Existing tenant compare and finding surfaces are tenant-first. They do not present one visible-set baseline view across all assigned tenants. - **Narrowest correct implementation**: Add one baseline-scoped matrix page, one narrow derived matrix builder, one centralized badge adapter that reuses canonical compare truth, and one compare-all fan-out path over existing tenant compare starts. - **Ownership cost created**: One new page, one view, one derived aggregation helper, one centralized badge adapter, additive baseline-detail actions, and focused feature plus browser regression coverage. - **Alternative intentionally rejected**: A persisted cross-tenant compare artifact, a generalized compare engine, or a workspace umbrella run were rejected because they add durable complexity beyond V1's read-only portfolio visibility goal. - **Release truth**: Current-release truth. The feature closes a present operator workflow gap using already-shipped baseline, finding, and run foundations.