TenantAtlas/specs/190-baseline-compare-matrix/plan.md
ahmido eca19819d1 feat: add workspace baseline compare matrix (#221)
## Summary
- add a workspace-scoped baseline compare matrix page under baseline profiles
- derive matrix tenant summaries, subject rows, cell states, freshness, and trust from existing snapshots, compare runs, and findings
- add confirmation-gated `Compare assigned tenants` actions on the baseline detail and matrix surfaces without introducing a workspace umbrella run
- preserve matrix navigation context into tenant compare and finding drilldowns and add centralized matrix badge semantics
- include spec, plan, data model, contracts, quickstart, tasks, and focused feature/browser coverage for Spec 190

## Verification
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- completed an integrated-browser smoke flow locally for matrix render, differ filter, finding drilldown round-trip, and `Compare assigned tenants` confirmation/action

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #221
2026-04-11 10:20:25 +00:00

272 lines
22 KiB
Markdown

# 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.