spec: complete baseline compare operator mode plan

This commit is contained in:
Ahmed Darrazi 2026-04-11 14:49:52 +02:00
parent 4c92ec1e64
commit 1075ed5ae3
7 changed files with 968 additions and 79 deletions

View File

@ -167,6 +167,8 @@ ## Active Technologies
- PostgreSQL via Laravel Eloquent with one new table `tenant_triage_reviews` and no new external caches or background stores (189-portfolio-triage-review-state)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns (190-baseline-compare-matrix)
- PostgreSQL via existing `baseline_profiles`, `baseline_snapshots`, `baseline_snapshot_items`, `baseline_tenant_assignments`, `operation_runs`, and `findings` tables; no new persistence planned (190-baseline-compare-matrix)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns (191-baseline-compare-operator-mode)
- PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned (191-baseline-compare-operator-mode)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -201,8 +203,7 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 191-baseline-compare-operator-mode: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
- 190-baseline-compare-matrix: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns
- 189-portfolio-triage-review-state: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `WorkspaceOverviewBuilder`, `TenantResource`, `TenantDashboard`, `PortfolioArrivalContext`, `TenantBackupHealthResolver`, `RestoreSafetyResolver`, `BadgeCatalog`, `UiEnforcement`, and `AuditRecorder` patterns
- 188-provider-connection-state-cleanup: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ProviderConnection` model, `ProviderConnectionResolver`, `ProviderConnectionStateProjector`, `ProviderConnectionMutationService`, `ProviderConnectionHealthCheckJob`, `StartVerification`, `ProviderConnectionResource`, `TenantResource`, system directory pages, `BadgeCatalog`, `BadgeRenderer`, and shared provider-state Blade entries
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@ -0,0 +1,501 @@
openapi: 3.1.0
info:
title: Baseline Compare Matrix Operator Mode Internal Surface Contract
version: 0.1.0
summary: Internal logical contract for adaptive operator-density rendering on the existing baseline compare matrix route
description: |
This contract is an internal planning artifact for Spec 191. The affected surface
still renders HTML through Filament and Livewire. The schemas below define the
bounded request-scoped presentation models and staged filter interactions that must
be derivable from existing Spec 190 matrix truth before the operator-density
refactor can render safely.
servers:
- url: /internal
x-baseline-compare-operator-mode-consumers:
- surface: baseline.compare.matrix
sourceFiles:
- apps/platform/app/Filament/Pages/BaselineCompareMatrix.php
- apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php
mustRender:
- reference
- presentation_state
- support_surface_state
- applied_filters
- tenant_summaries
- dense_rows_or_compact_results
- last_updated_at
- auto_refresh_state
mustAccept:
- mode
- policy_type
- state
- severity
- tenant_sort
- subject_sort
- subject_key
mustStage:
- selectedPolicyTypes
- selectedStates
- selectedSeverities
paths:
/admin/baseline-profiles/{profile}/compare-matrix:
get:
summary: Render the existing baseline compare matrix using adaptive operator-density presentation
operationId: viewBaselineCompareOperatorMode
parameters:
- name: profile
in: path
required: true
schema:
type: integer
- name: mode
in: query
required: false
schema:
$ref: '#/components/schemas/PresentationMode'
- name: policy_type
in: query
required: false
schema:
type: array
items:
type: string
- name: state
in: query
required: false
schema:
type: array
items:
$ref: '#/components/schemas/MatrixCellState'
- name: severity
in: query
required: false
schema:
type: array
items:
$ref: '#/components/schemas/FindingSeverity'
- name: tenant_sort
in: query
required: false
schema:
type: string
- name: subject_sort
in: query
required: false
schema:
type: string
- name: subject_key
in: query
required: false
schema:
type: string
responses:
'200':
description: Rendered matrix plus adaptive operator-density read models
content:
text/html:
schema:
type: string
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
schema:
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
'403':
description: Actor is in scope but lacks workspace baseline view capability
'404':
description: Workspace or baseline profile is outside actor scope
/internal/workspaces/{workspace}/baseline-profiles/{profile}/compare-matrix/apply-filters:
post:
summary: Apply staged heavy filters to the operator-density matrix route
operationId: applyBaselineCompareOperatorFilters
parameters:
- name: workspace
in: path
required: true
schema:
type: integer
- name: profile
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MatrixFilterDraft'
responses:
'200':
description: Updated operator-density bundle using the applied filter state
content:
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
schema:
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
'403':
description: Actor is in scope but lacks workspace baseline view capability
'404':
description: Workspace or baseline profile is outside actor scope
/internal/workspaces/{workspace}/baseline-profiles/{profile}/compare-matrix/reset-filters:
post:
summary: Reset staged and applied heavy filters for the operator-density matrix route
operationId: resetBaselineCompareOperatorFilters
parameters:
- name: workspace
in: path
required: true
schema:
type: integer
- name: profile
in: path
required: true
schema:
type: integer
responses:
'200':
description: Updated operator-density bundle with default filter state restored
content:
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
schema:
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
'403':
description: Actor is in scope but lacks workspace baseline view capability
'404':
description: Workspace or baseline profile is outside actor scope
components:
schemas:
PresentationMode:
type: string
enum:
- auto
- dense
- compact
MatrixCellState:
type: string
enum:
- match
- differ
- missing
- ambiguous
- not_compared
- stale_result
FindingSeverity:
type: string
enum:
- low
- medium
- high
- critical
FreshnessState:
type: string
enum:
- fresh
- stale
- never_compared
- unknown
TrustLevel:
type: string
enum:
- trustworthy
- limited_confidence
- diagnostic_only
- unusable
AttentionLevel:
type: string
enum:
- aligned
- review
- refresh_recommended
- needs_attention
MatrixReference:
type: object
additionalProperties: false
required:
- baselineProfileId
- baselineProfileName
- referenceState
- assignedTenantCount
- visibleTenantCount
properties:
baselineProfileId:
type: integer
baselineProfileName:
type: string
referenceSnapshotId:
type:
- integer
- 'null'
referenceState:
type: string
assignedTenantCount:
type: integer
visibleTenantCount:
type: integer
MatrixFilterDraft:
type: object
additionalProperties: false
required:
- selectedPolicyTypes
- selectedStates
- selectedSeverities
- tenantSort
- subjectSort
properties:
selectedPolicyTypes:
type: array
items:
type: string
selectedStates:
type: array
items:
$ref: '#/components/schemas/MatrixCellState'
selectedSeverities:
type: array
items:
$ref: '#/components/schemas/FindingSeverity'
tenantSort:
type: string
subjectSort:
type: string
focusedSubjectKey:
type:
- string
- 'null'
MatrixPresentationState:
type: object
additionalProperties: false
required:
- requestedMode
- resolvedMode
- visibleTenantCount
- activeFilterCount
- hasStagedFilterChanges
- autoRefreshActive
- canOverrideMode
properties:
requestedMode:
$ref: '#/components/schemas/PresentationMode'
resolvedMode:
type: string
enum:
- dense
- compact
visibleTenantCount:
type: integer
activeFilterCount:
type: integer
hasStagedFilterChanges:
type: boolean
autoRefreshActive:
type: boolean
lastUpdatedAt:
type:
- string
- 'null'
format: date-time
canOverrideMode:
type: boolean
MatrixTenantSummary:
type: object
additionalProperties: false
required:
- tenantId
- tenantName
- freshnessState
- differingCount
- missingCount
- ambiguousCount
- trustLevel
properties:
tenantId:
type: integer
tenantName:
type: string
freshnessState:
$ref: '#/components/schemas/FreshnessState'
lastComparedAt:
type:
- string
- 'null'
format: date-time
differingCount:
type: integer
missingCount:
type: integer
ambiguousCount:
type: integer
trustLevel:
$ref: '#/components/schemas/TrustLevel'
maxSeverity:
type:
- string
- 'null'
DenseCellView:
type: object
additionalProperties: false
required:
- tenantId
- subjectKey
- state
- freshnessState
- trustLevel
- attentionLevel
properties:
tenantId:
type: integer
subjectKey:
type: string
state:
$ref: '#/components/schemas/MatrixCellState'
freshnessState:
$ref: '#/components/schemas/FreshnessState'
trustLevel:
$ref: '#/components/schemas/TrustLevel'
severity:
type:
- string
- 'null'
attentionLevel:
$ref: '#/components/schemas/AttentionLevel'
reasonSummary:
type:
- string
- 'null'
primaryDrilldownUrl:
type:
- string
- 'null'
secondaryDrilldownUrls:
type: object
additionalProperties:
type: string
DenseSubjectRowView:
type: object
additionalProperties: false
required:
- subjectKey
- displayName
- policyType
- deviationBreadth
- missingBreadth
- ambiguousBreadth
- trustLevel
- cells
properties:
subjectKey:
type: string
displayName:
type: string
policyType:
type: string
baselineExternalId:
type:
- string
- 'null'
deviationBreadth:
type: integer
missingBreadth:
type: integer
ambiguousBreadth:
type: integer
maxSeverity:
type:
- string
- 'null'
trustLevel:
$ref: '#/components/schemas/TrustLevel'
cells:
type: array
items:
$ref: '#/components/schemas/DenseCellView'
CompactSubjectResultView:
type: object
additionalProperties: false
required:
- tenantId
- subjectKey
- displayName
- policyType
- state
- freshnessState
- trustLevel
properties:
tenantId:
type: integer
subjectKey:
type: string
displayName:
type: string
policyType:
type: string
state:
$ref: '#/components/schemas/MatrixCellState'
freshnessState:
$ref: '#/components/schemas/FreshnessState'
trustLevel:
$ref: '#/components/schemas/TrustLevel'
severity:
type:
- string
- 'null'
reasonSummary:
type:
- string
- 'null'
primaryDrilldownUrl:
type:
- string
- 'null'
runUrl:
type:
- string
- 'null'
MatrixSupportSurfaceState:
type: object
additionalProperties: false
required:
- legendMode
- showActiveFilterSummary
- showLastUpdated
- showAutoRefreshHint
- showBlockingRefreshState
properties:
legendMode:
type: string
showActiveFilterSummary:
type: boolean
showLastUpdated:
type: boolean
showAutoRefreshHint:
type: boolean
showBlockingRefreshState:
type: boolean
BaselineCompareOperatorModeBundle:
type: object
additionalProperties: false
required:
- reference
- presentation
- supportSurface
- appliedFilters
- tenantSummaries
properties:
reference:
$ref: '#/components/schemas/MatrixReference'
presentation:
$ref: '#/components/schemas/MatrixPresentationState'
supportSurface:
$ref: '#/components/schemas/MatrixSupportSurfaceState'
appliedFilters:
$ref: '#/components/schemas/MatrixFilterDraft'
tenantSummaries:
type: array
items:
$ref: '#/components/schemas/MatrixTenantSummary'
denseRows:
type: array
items:
$ref: '#/components/schemas/DenseSubjectRowView'
compactResults:
type: array
items:
$ref: '#/components/schemas/CompactSubjectResultView'

View File

@ -0,0 +1,166 @@
# Data Model: Baseline Compare Matrix: High-Density Operator Mode
## Overview
This follow-up introduces no new persisted entity. It reuses the existing Spec 190 matrix truth and adds derived presentation models for operator density, staged filtering, and non-blocking status cues.
## Existing Source Truths Reused Without Change
### Baseline compare truth from Spec 190
The following derived or canonical inputs remain authoritative and are not redefined by this spec:
- workspace-scoped baseline reference truth
- visible tenant summaries
- subject summaries
- subject-by-tenant matrix cells
- compare-start availability and existing drilldown destinations
This spec changes how those inputs are rendered and interacted with, not how they are computed.
## New Derived Presentation Models
### MatrixPresentationState
**Type**: request-scoped page presentation contract
**Source**: route/query state + visible tenant count + existing run state
| Field | Type | Notes |
|------|------|-------|
| `requestedMode` | string | `auto`, `dense`, or `compact` from route/query state |
| `resolvedMode` | string | Final mode used for rendering: `dense` or `compact` |
| `visibleTenantCount` | integer | Existing visible-set count from the matrix bundle |
| `activeFilterCount` | integer | Count of currently applied filters |
| `hasStagedFilterChanges` | boolean | Whether filter draft state differs from applied state |
| `autoRefreshActive` | boolean | True when background polling is active because compare work is queued or running |
| `lastUpdatedAt` | datetime or null | Timestamp for the currently rendered matrix data |
| `canOverrideMode` | boolean | Whether the operator may locally switch away from `auto` |
### MatrixFilterDraft
**Type**: request-scoped staged filter model
**Source**: page form state only
| Field | Type | Notes |
|------|------|-------|
| `selectedPolicyTypes` | array<string> | Draft policy-type filter selection |
| `selectedStates` | array<string> | Draft state-group selection |
| `selectedSeverities` | array<string> | Draft severity selection |
| `tenantSort` | string | Current tenant sort choice |
| `subjectSort` | string | Current subject sort choice |
| `focusedSubjectKey` | string or null | Optional current subject focus |
### DenseSubjectRowView
**Type**: request-scoped dense-mode row view
**Source**: existing subject summary + existing matrix cells
| Field | Type | Notes |
|------|------|-------|
| `subjectKey` | string | Stable row key |
| `displayName` | string | Primary row label |
| `policyType` | string | Compact secondary label |
| `baselineExternalId` | string or null | Optional secondary context |
| `deviationBreadth` | integer | Existing subject summary metric |
| `missingBreadth` | integer | Existing subject summary metric |
| `ambiguousBreadth` | integer | Existing subject summary metric |
| `maxSeverity` | string or null | Existing subject summary severity |
| `trustLevel` | string | Existing subject summary trust |
| `cells` | array<DenseCellView> | One condensed cell per visible tenant |
### DenseCellView
**Type**: request-scoped dense-mode cell view
**Source**: existing matrix cell + existing tenant summary freshness
| Field | Type | Notes |
|------|------|-------|
| `tenantId` | integer | Visible tenant identifier |
| `subjectKey` | string | Subject row key |
| `state` | string | Existing Spec 190 state |
| `freshnessState` | string | Freshness signal shown in compact form |
| `trustLevel` | string | Trust signal shown in compact form |
| `severity` | string or null | Optional attention signal |
| `attentionLevel` | string | Derived presentation label such as `aligned`, `refresh_recommended`, or `needs_attention` |
| `reasonSummary` | string or null | Short secondary explanation for compact reveal surfaces |
| `primaryDrilldownUrl` | string or null | Preferred next follow-up action |
| `secondaryDrilldownUrls` | array<string, string> | Additional compact follow-up links when available |
### CompactSubjectResultView
**Type**: request-scoped single-tenant row view
**Source**: one visible tenant summary + existing matrix cell + existing subject summary
| Field | Type | Notes |
|------|------|-------|
| `tenantId` | integer | The single visible tenant in compact mode |
| `subjectKey` | string | Stable subject key |
| `displayName` | string | Primary subject label |
| `policyType` | string | Secondary grouping/context |
| `state` | string | Existing Spec 190 state |
| `freshnessState` | string | Compact freshness label |
| `trustLevel` | string | Compact trust label |
| `severity` | string or null | Optional attention indicator |
| `reasonSummary` | string or null | Short explanation line |
| `primaryDrilldownUrl` | string or null | Main follow-up action |
| `runUrl` | string or null | Secondary run-level follow-up |
### MatrixSupportSurfaceState
**Type**: request-scoped supporting-context contract
**Source**: page state + existing legends + refresh metadata
| Field | Type | Notes |
|------|------|-------|
| `legendMode` | string | `grouped`, `collapsed`, or equivalent compact support behavior |
| `showActiveFilterSummary` | boolean | Whether applied filters are summarized inline |
| `showLastUpdated` | boolean | Whether the page displays last-updated metadata |
| `showAutoRefreshHint` | boolean | Whether passive auto-refresh copy is visible |
| `showBlockingRefreshState` | boolean | Reserved for deliberate user-triggered reloads only |
## Rendering and Resolution Rules
### Mode resolution rules
1. If `requestedMode = auto` and `visibleTenantCount > 1`, resolve to `dense`.
2. If `requestedMode = auto` and `visibleTenantCount = 1`, resolve to `compact`.
3. If a manual override is present, use it unless it would produce an invalid empty layout.
4. Manual override remains route-local and must never be persisted as product truth.
### Dense-mode rules
- The subject column remains sticky during horizontal scroll.
- The primary visible content per cell is state, trust, freshness, and attention.
- Long explanatory text and repeated action links do not render as the dominant cell body.
### Compact single-tenant rules
- The tenant header does not repeat as a pseudo-column structure.
- Each subject entry shows one primary status line and a reduced set of secondary metadata.
- Existing subject focus and drilldown continuity remain available.
### Filter workflow rules
- Heavy multi-select filters use staged state first and apply only when the operator confirms.
- Applied filter count and scope summary reflect the applied state, not merely the draft state.
- Reset may clear both draft and applied state in one explicit action.
### Status signal rules
- `blocking refresh` is reserved for deliberate user-triggered reload or recalculation moments.
- `auto-refresh active` indicates passive polling while compare work is still queued or running.
- `lastUpdatedAt` reflects the timestamp of the rendered matrix payload, not merely the latest compare run in the system.
### Safety rules
- No rendering path may widen tenant visibility beyond the existing visible set.
- No presentation-state change may change the underlying compare state, trust, or freshness semantics.
- No grouped legend or compact cell may invent new status vocabulary outside existing centralized badge semantics.
## Relationships
- One `MatrixPresentationState` governs one rendered matrix page.
- One `MatrixFilterDraft` belongs to one `MatrixPresentationState`.
- In dense mode, one `DenseSubjectRowView` maps to many `DenseCellView` entries.
- In compact mode, one visible tenant yields many `CompactSubjectResultView` entries.
- One `MatrixSupportSurfaceState` coordinates legends, refresh hints, and active-filter summaries for the same page render.

View File

@ -3,64 +3,108 @@ # Implementation Plan: Baseline Compare Matrix: High-Density Operator Mode
**Branch**: `191-baseline-compare-operator-mode` | **Date**: 2026-04-11 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/spec.md`
**Note**: This plan formalizes the existing 191 spec slice and keeps the work strictly inside the already-shipped Spec 190 matrix surface.
## Summary
Rework the existing baseline compare matrix route into an operator-density follow-up to Spec 190. The route stays workspace-scoped and fully derived, but gains adaptive presentation rules: dense multi-tenant scanning when several visible tenants are present, compact single-tenant comparison when only one visible tenant remains, and calmer filter, legend, action, and refresh surfaces.
Refactor the existing workspace baseline compare matrix into an adaptive operator-density surface. The route, baseline reference, visible-set-only truth, compare-start behavior, and drilldowns stay unchanged, but the page gains local presentation-mode state, dense multi-tenant scanning, compact single-tenant rendering, staged heavy-filter application, grouped legends, and clearer separation between blocking refresh, passive auto-refresh, and last-updated status.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
**Storage**: Existing PostgreSQL truth only; no new tables or artifacts
**Testing**: Pest feature tests and one browser smoke path through Sail
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
**Storage**: PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned
**Testing**: Pest feature tests and browser smoke coverage run through Laravel Sail
**Target Platform**: Laravel monolith web application under `apps/platform`
**Project Type**: web application
**Performance Goals**: Improve operator scan throughput without adding more data queries than Spec 190; keep heavy filter changes explicit rather than chatty
**Constraints**: No compare-logic changes, no new persistence, no hidden-tenant leakage, no generalized UI framework, no Filament provider changes
**Scale/Scope**: One existing matrix page, one existing view, one existing builder, and focused test coverage updates
**Performance Goals**: Improve scan throughput without increasing query shape beyond Spec 190, keep heavy filter changes non-chatty, and preserve DB-only render-time matrix surfaces
**Constraints**: No compare-logic change, no new persistence, no hidden-tenant leakage, no generalized density framework, no provider or panel changes, and no new asset pipeline
**Scale/Scope**: One existing matrix page, one existing Blade view, one existing builder, one logical contract file, and focused feature plus browser regressions
## Constitution Check
*GATE: Passed before design. No new source-of-truth or persistence changes are expected.*
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.*
| Principle | Status | Notes |
|-----------|--------|-------|
| Inventory-first / snapshots-second | PASS | The feature changes presentation only and keeps Spec 190 truth sources intact. |
| Read/write separation | PASS | `Compare assigned tenants` remains the only mutation and already exists. |
| Workspace + tenant isolation | PASS | Visible-set-only behavior remains unchanged. |
| RBAC-UX | PASS | Existing `404` vs `403` semantics stay intact; only presentation changes. |
| Ops-UX 3-surface feedback | PASS | Refresh and polling surfaces are clarified visually without changing run semantics. |
| Proportionality / anti-bloat | PASS | No new persistence, enum, framework, or cross-domain abstraction is introduced. |
| UI semantics / few layers | PASS | Dense and compact modes reuse existing badge and compare semantics rather than inventing new status taxonomies. |
| Filament v5 / Livewire v4 compliance | PASS | Work remains on the existing Filament page and Livewire-backed route. |
| Provider registration location | PASS | No provider changes; registration remains in `bootstrap/providers.php`. |
| Global search hard rule | PASS | No new global-searchable resource or page is introduced. |
| Destructive action safety | PASS | No destructive action is added by this spec. |
| Asset strategy | PASS | No new panel assets or shared assets are required. Existing deployment use of `filament:assets` remains unchanged. |
| Principle | Pre-Research | Post-Design | Notes |
|-----------|--------------|-------------|-------|
| Inventory-first / snapshots-second | PASS | PASS | The spec changes presentation only and keeps Spec 190 truth sources intact. |
| Read/write separation | PASS | PASS | `Compare assigned tenants` remains the only mutation and is unchanged. |
| Graph contract path | N/A | N/A | No new Graph behavior or contract-registry work is introduced. |
| Deterministic capabilities | PASS | PASS | Existing capabilities remain canonical and unchanged. |
| Workspace + tenant isolation | PASS | PASS | Visible-set-only aggregation and drilldown scope remain unchanged. |
| RBAC-UX authorization semantics | PASS | PASS | Existing `404` vs `403` semantics and server-side enforcement remain unchanged. |
| Run observability / Ops-UX | PASS | PASS | Compare-run truth is reused exactly as in Spec 190; this spec only clarifies the visual cues around it. |
| Data minimization | PASS | PASS | No new data copies, exports, or persisted UI artifacts are introduced. |
| Proportionality / anti-bloat | PASS | PASS | The work stays local to one page and does not add a new abstraction or stored artifact. |
| Persisted truth / behavioral state | PASS | PASS | Presentation mode and staged filter state remain request-scoped only. |
| UI semantics / few layers | PASS | PASS | Existing state, trust, freshness, and severity semantics are reused rather than redefined. |
| Filament v5 / Livewire v4 compliance | PASS | PASS | The work remains inside the existing Filament page and Livewire-backed route. |
| Provider registration location | PASS | PASS | No provider changes are required; Laravel 11+ registration remains in `bootstrap/providers.php`. |
| Global search hard rule | PASS | PASS | No new searchable resource or page is introduced. |
| Destructive action safety | PASS | PASS | No destructive action is added. Existing confirmation behavior for compare-start remains unchanged. |
| Asset strategy | PASS | PASS | No new assets are required. Existing deployment behavior for `cd apps/platform && php artisan filament:assets` remains unchanged. |
## Filament-Specific Compliance Notes
- **Livewire v4.0+ compliance**: This plan stays on the existing Filament v5 + Livewire v4 page stack and does not introduce legacy APIs.
- **Provider registration location**: No panel/provider work is needed. Laravel 11+ provider registration remains in `bootstrap/providers.php`.
- **Global search**: This spec does not add a new globally searchable resource. Existing baseline-resource search behavior is unchanged.
- **Livewire v4.0+ compliance**: This plan remains on Filament v5 + Livewire v4 and does not introduce legacy APIs.
- **Provider registration location**: No panel or provider changes are needed; Laravel 11+ provider registration remains in `bootstrap/providers.php`.
- **Global search**: The feature does not add a new globally searchable resource. Existing baseline-resource search behavior is unchanged.
- **Destructive actions**: No new destructive action is introduced. Existing compare-start actions remain confirmation-gated where already defined.
- **Asset strategy**: No new global or on-demand asset registration is planned. Deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged.
- **Testing plan**: Extend the existing matrix feature and browser suites to cover presentation mode, density, compact controls, and non-blocking status surfaces.
- **Testing plan**: Extend the existing matrix feature, builder, guard, and browser suites to cover presentation mode, staged filter application, and non-blocking status surfaces.
## Phase 0 Research
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/research.md`.
Key decisions:
- Keep the existing matrix route and truth model and change presentation only.
- Resolve `auto`, `dense`, and `compact` mode from visible tenant count, with a route-local override only.
- Make dense mode state-first rather than action-first.
- Render single-tenant review as a compact compare list rather than a one-column matrix.
- Convert heavy filters to staged apply/reset semantics.
- Replace the long policy-type checkbox stack with a more compact operator-first selector.
- Group legends into compact support context and separate blocking refresh from passive auto-refresh and last-updated cues.
- Reuse existing drilldown and visible-set semantics unchanged.
## Phase 1 Design
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/`:
- `research.md`: decisions and rejected alternatives for local operator-density work
- `data-model.md`: request-scoped presentation models for mode state, staged filters, dense rows, compact results, and support-surface state
- `contracts/baseline-compare-operator-mode.logical.openapi.yaml`: internal logical contract for adaptive rendering and staged filter application
- `quickstart.md`: implementation and verification sequence for the follow-up spec
Design decisions:
- `auto` remains the default requested mode and resolves to `dense` for multiple visible tenants and `compact` for exactly one visible tenant.
- Manual mode override remains route-local and must never become stored product truth.
- Dense mode reuses existing compare truth but condenses cell content to state, trust, freshness, and attention.
- Compact mode reuses the same truth but removes pseudo-matrix structure once only one visible tenant remains.
- Heavy filter inputs stage locally and apply explicitly; lightweight route-state changes may remain immediate.
- Grouped legends, passive auto-refresh, and last-updated signals become support context rather than competing top-level content.
## Project Structure
### Documentation
### Documentation (this feature)
```text
specs/191-baseline-compare-operator-mode/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── spec.md
├── tasks.md
├── contracts/
│ └── baseline-compare-operator-mode.logical.openapi.yaml
└── checklists/
└── requirements.md
```
### Source Code
### Source Code (repository root)
```text
apps/platform/
@ -77,84 +121,80 @@ ### Source Code
├── Browser/
│ └── Spec190BaselineCompareMatrixSmokeTest.php
├── Feature/
│ ├── Baselines/
│ │ └── BaselineCompareMatrixBuilderTest.php
│ ├── Filament/
│ │ └── BaselineCompareMatrixPageTest.php
│ └── Guards/
│ └── ActionSurfaceContractTest.php
└── Feature/Baselines/
└── BaselineCompareMatrixBuilderTest.php
└── Unit/
└── Badges/
```
**Structure Decision**: Keep the work inside the existing Spec 190 implementation surface. This follow-up spec is a refactor of one page and its supporting builder/view behavior, not a new domain slice.
**Structure Decision**: Keep the work inside the existing Spec 190 matrix implementation surface. This is a presentation refactor of one existing page and its supporting builder/view behavior, not a new domain slice or a new application area.
## Key Design Decisions
## Complexity Tracking
### D-001 — Keep the route and truth model unchanged
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | N/A | The follow-up stays within the existing page, builder, and test surfaces and introduces no new structural violation. |
This spec modifies the existing `/compare-matrix` route only. No second route, second matrix artifact, or separate dense-report model is created.
## Proportionality Review
### D-002 — Use adaptive presentation, not separate features
`auto` mode is the canonical default. `dense` and `compact` exist as local operator overrides, but the product concept remains one matrix page with adaptive presentation.
### D-003 — Keep dense cells state-first
Dense mode cells must prioritize compare state, trust, freshness, and attention. Detailed reasons and multiple navigation targets become secondary reveals rather than permanent default chrome.
### D-004 — Treat controls as supporting context
Filters, legends, and refresh hints remain available but must become visibly subordinate to the matrix body. The page should read as a working surface, not a form-first screen.
### D-005 — Keep single-tenant mode honest
If only one visible tenant remains, the operator should see a compact comparison surface rather than an artificially wide matrix. The page should not preserve multi-tenant structure when it no longer helps.
- **Current operator problem**: The matrix already answers the right governance question, but not with enough density or calmness for repeated operator scanning.
- **Existing structure is insufficient because**: The current single rendering shape tries to serve both multi-tenant and single-tenant workflows, so supporting context, action repetition, and cell chrome are too heavy in both cases.
- **Narrowest correct implementation**: Keep the same route, truth sources, drilldowns, and compare semantics while adding route-local presentation state, denser rendering, and staged filter application.
- **Ownership cost created**: Additional view-state logic, a logical contract file, and focused regression coverage for mode resolution, filter workflow, and status visibility.
- **Alternative intentionally rejected**: A generalized density framework, a separate dense-report route, or a stored matrix artifact were rejected because the problem is local to the existing matrix surface.
- **Release truth**: current-release operator workflow compression
## Implementation Strategy
### Phase A — Presentation Mode Contract
- Add `auto`, `dense`, and `compact` mode state to the page.
- Keep override state local to the route and compatible with existing drilldown URLs.
- Reuse the current derived matrix bundle instead of adding a second persisted view model.
- Add route-local `auto`, `dense`, and `compact` mode state.
- Resolve the active mode from visible tenant count unless manually overridden.
- Expose `lastUpdatedAt`, `hasStagedFilterChanges`, and passive auto-refresh state to the page.
### Phase B — Dense Multi-Tenant Surface
- Reduce per-cell chrome and prioritize state/trust/freshness.
- Keep the subject axis sticky and readable across horizontal scroll.
- Move repeated actions into compact secondary affordances where necessary.
- Keep the subject column sticky during horizontal scroll.
- Condense dense cells to state, trust, freshness, and attention signals.
- Move repeated actions into compact secondary affordances without breaking drilldown continuity.
### Phase C — Compact Single-Tenant Surface
- Replace pseudo-matrix presentation with a shorter, calmer list optimized for one visible tenant.
- Remove repeated tenant headers and duplicated labels.
- Preserve subject focus and drilldowns.
- Replace pseudo-matrix rendering with a compact subject-result list when only one visible tenant remains.
- Remove repeated tenant headers and duplicated secondary metadata.
- Preserve subject focus and the existing compare/finding/run destinations.
### Phase D — Supporting Context Compression
- Convert heavy filters to an apply/reset workflow.
- Compress legends into grouped or collapsible supporting blocks.
- Clarify background polling, manual refresh, and last-updated status without using blocking loading surfaces.
- Convert heavy matrix filters to staged apply/reset behavior.
- Replace the current long policy-type control with a more compact selector.
- Group or collapse legends.
- Separate blocking refresh from passive auto-refresh and last-updated status.
### Phase E — Verification
- Extend feature coverage for mode selection and density rules.
- Extend browser coverage for one dense-mode path and one compact-mode path.
- Keep existing Spec 190 truth and RBAC guarantees intact.
- Extend focused feature coverage for mode resolution, staged filter behavior, and support-surface state.
- Extend browser smoke coverage for one dense-mode path and one compact-mode path.
- Keep existing Spec 190 matrix truth, drilldown continuity, and RBAC guarantees green.
## Risks & Mitigations
## Risk Assessment
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Dense mode becomes another framework | Medium | Low | Keep presentation logic local to the matrix page and view. |
| Compact mode hides too much drilldown value | Medium | Medium | Keep one clear follow-up path per subject and preserve existing drilldowns. |
| Apply/reset feels stale compared with live filters | Medium | Medium | Make staged filter state obvious and keep reset immediate. |
| Manual override confuses operators | Low | Medium | Keep `auto` as default and label override state clearly. |
| Dense mode becomes another framework | Medium | Low | Keep presentation logic local to the matrix page and avoid generalized shared abstractions. |
| Compact mode hides too much follow-up value | Medium | Medium | Preserve one clear primary drilldown per subject and keep existing follow-up destinations intact. |
| Staged filtering feels slow or unclear | Medium | Medium | Show explicit staged/applied state and keep reset obvious. |
| Manual override confuses operators | Low | Medium | Keep `auto` as the default and surface the resolved mode clearly. |
| Last-updated and auto-refresh cues drift out of sync | Medium | Low | Derive both cues from the same rendered matrix payload and active-run state. |
## Test Strategy
- Extend feature tests for mode resolution based on visible tenant count.
- Add assertions for dense multi-tenant sticky subject behavior and reduced visible action noise.
- Add assertions for compact single-tenant rendering and shorter supporting chrome.
- Add coverage for explicit filter apply/reset behavior, grouped legends, and page-level last-updated messaging.
- Reuse existing browser smoke coverage and extend it for one dense path plus one compact-mode path.
- Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` and the focused matrix-related Pest suite before sign-off.
- Extend `BaselineCompareMatrixPageTest` for requested vs resolved mode, active filter application, compact vs dense rendering, and non-blocking refresh cues.
- Extend `BaselineCompareMatrixBuilderTest` for any new derived presentation metadata required by the page.
- Keep `ActionSurfaceContractTest` green so calmer actions do not regress the surface contract.
- Extend `Spec190BaselineCompareMatrixSmokeTest` to prove one dense-mode and one compact-mode operator path on the Livewire page.
- Run the focused Sail verification pack from `quickstart.md` and re-run `update-agent-context.sh copilot` after the plan is finalized.

View File

@ -0,0 +1,70 @@
# Quickstart: Baseline Compare Matrix: High-Density Operator Mode
## Goal
Turn the existing baseline compare matrix into a denser operator surface without changing its underlying compare truth. Multi-tenant use should favor high-density cross-tenant scanning, while single-tenant use should collapse into a calmer compact comparison view.
## Implementation Sequence
1. Add page-level presentation state.
- Add `auto`, `dense`, and `compact` route-local mode state.
- Resolve the active mode from visible tenant count unless the operator explicitly overrides it.
- Expose `lastUpdatedAt`, staged-filter state, and passive auto-refresh state on the page.
2. Build the dense multi-tenant rendering contract.
- Keep the subject column sticky.
- Reduce dense-cell chrome to state, trust, freshness, and attention.
- Move repeated follow-up links into compact secondary affordances.
3. Build the compact single-tenant rendering contract.
- Replace the pseudo-matrix layout with a compact subject-result list.
- Remove repeated tenant headers and repeated metadata blocks.
- Preserve subject focus and existing drilldowns.
4. Compress supporting context.
- Convert heavy filters to staged apply/reset semantics.
- Replace the current long policy-type list with a more compact operator-first control.
- Group or collapse legends so they remain available without dominating the page.
- Separate blocking refresh from passive auto-refresh and last-updated status.
5. Extend regression coverage.
- Cover mode resolution, dense multi-tenant layout, compact single-tenant layout, staged filters, and non-blocking refresh cues.
- Keep existing Spec 190 matrix truth, drilldown continuity, and RBAC guarantees green.
## Suggested Test Files
- `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`
- `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`
- `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
- `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
## Minimum Verification Commands
Run all commands through Sail from `apps/platform`.
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareMatrixPageTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
## Manual Acceptance Checklist
1. Open a baseline profile whose matrix has multiple visible tenants and confirm `auto` resolves to dense mode.
2. Verify the first subject column remains visible while horizontally scrolling dense mode.
3. Confirm dense cells foreground compare state, trust, freshness, and attention before links or long prose.
4. Open a matrix that resolves to one visible tenant and confirm `auto` resolves to compact mode instead of a one-column matrix.
5. Change heavy filters and confirm the page stages those changes until the operator applies them.
6. Confirm active filter count and filter summary reflect the applied state clearly.
7. Confirm legends are still understandable but no longer dominate the top of the page.
8. Trigger or observe queued/running compare work and confirm passive auto-refresh does not look like a permanent blocking load.
9. Confirm the page shows when the current matrix payload was last updated.
10. Verify tenant compare, finding, and run drilldowns still preserve the existing matrix context.
## Deployment Notes
- No migration is expected.
- No new asset registration is expected.
- No queue topology change is expected because compare execution semantics stay unchanged.

View File

@ -0,0 +1,111 @@
# Research: Baseline Compare Matrix: High-Density Operator Mode
## Decision: Keep the existing matrix route and truth model, and change presentation only
### Rationale
Spec 190 already established the correct workspace route, the correct baseline reference model, and the correct visible-set-only compare truth. The operator-density follow-up should stay on `/admin/baseline-profiles/{record}/compare-matrix` and must not introduce a second route, a second report artifact, or a second source of matrix truth.
### Alternatives considered
- Add a separate `dense report` page: rejected because it would duplicate the same baseline-scoped workflow on a second route.
- Add a stored matrix snapshot: rejected because the operator problem is scan efficiency, not missing persistence.
## Decision: Resolve presentation mode from visible tenant count, with a local override only
### Rationale
The core operator split is real: one visible tenant is a compact review problem, while several visible tenants create a cross-tenant scan problem. The narrowest implementation is one requested mode (`auto`, `dense`, or `compact`) and one resolved mode at render time. `auto` should remain the default, while manual override stays local to the matrix route and must not become stored user preference or domain truth.
### Alternatives considered
- Separate feature flags or separate navigation entries for each mode: rejected because the matrix should remain one operator surface.
- Persist mode preference per user: rejected because the current need is local workflow control, not profile-level personalization.
## Decision: Dense mode must be state-first, not action-first
### Rationale
In multi-tenant reading, the primary questions are where drift exists, how severe it is, whether the signal is trustworthy, and what deserves follow-up next. Dense cells should therefore foreground compare state, trust, freshness, and attention, while detailed reasons and repeated links move into compact secondary affordances.
### Alternatives considered
- Keep the current repeated open-link pattern in every cell: rejected because repeated actions visually outrank the state being scanned.
- Remove cell-level follow-up completely: rejected because the matrix must remain a decision surface, not a dead-end report.
## Decision: Single-tenant mode should be a compact compare list, not a one-column matrix
### Rationale
Once only one visible tenant remains, the value of cross-tenant columns disappears. The surface should switch to a shorter subject-result list that reuses the same truth but removes repeated tenant headers, empty width, and oversized cell chrome.
### Alternatives considered
- Reuse dense mode even for one tenant: rejected because it preserves the wrong reading model.
- Route single-tenant viewing away to the tenant compare page: rejected because the operator still started from the workspace baseline matrix context and should not lose that context automatically.
## Decision: Heavy filters should use staged apply/reset semantics
### Rationale
The current matrix is dense enough that chatty recomputation on every multi-select click works against operator flow. Policy types and other heavy matrix filters should stage changes locally, show that staged state clearly, and apply them deliberately. This improves calmness and makes the surface feel less like a form page.
### Alternatives considered
- Keep all filters live: rejected because heavy multi-select controls create noisy redraw behavior.
- Convert every filter to manual apply: rejected because lightweight interactions such as mode switching or focused-subject clearing should remain immediate.
## Decision: Replace the long policy-type checkbox stack with a more compact operator-first selector
### Rationale
The policy-type filter is the most visually expensive control on the page. The follow-up spec should use a denser selection pattern such as searchable multi-select, type-to-find, or another compact control that exposes the same filter truth without the current long vertical list.
### Alternatives considered
- Keep the long checkbox list and only restyle it: rejected because vertical space is the actual product problem.
- Hide policy type filtering behind a modal by default: rejected because the filter remains core enough to deserve immediate access.
## Decision: Legends should become grouped support context, optionally collapsible
### Rationale
State, freshness, and trust legends remain semantically valuable, especially for onboarding or occasional operators, but they should no longer compete with the matrix for top-of-screen attention. Grouped, compact legend blocks are the narrowest way to preserve semantics while reducing dominance.
### Alternatives considered
- Remove legends entirely: rejected because trust and freshness semantics still need an on-page reference.
- Leave three separate full-width legend sections: rejected because they displace the primary working surface.
## Decision: Separate loading, auto-refresh, and last-updated cues
### Rationale
Spec 190 already exposed the risk of background polling reading like permanent blocking load. This follow-up should make three states explicit: active loading for user-triggered refresh, passive auto-refresh while queued or running compare work exists, and last-updated time for the currently rendered matrix.
### Alternatives considered
- Reuse one generic refresh chip for all states: rejected because operators cannot tell whether the page is blocked or simply polling.
- Hide refresh state entirely: rejected because operator trust depends on understanding when the matrix is current.
## Decision: Reuse the existing drilldown and visible-set semantics without change
### Rationale
This spec is a presentation refactor, not a navigation or authorization redesign. The existing tenant compare, finding, run-detail, and canonical-navigation context from Spec 190 remain correct and should carry forward unchanged.
### Alternatives considered
- Introduce a dense-mode-specific drilldown model: rejected because it would create new behavior where existing follow-up paths are already sufficient.
- Add aggregated hidden-tenant remainder summaries: rejected because visible-set-only semantics explicitly avoid hidden-tenant leakage.
## Decision: Validate primarily with focused page, builder, guard, and browser coverage
### Rationale
The highest-risk changes are mode resolution, dense-cell hierarchy, compact single-tenant rendering, filter apply behavior, and non-blocking refresh cues. These are best covered with focused feature tests plus one browser smoke path for the interactive Livewire surface.
### Alternatives considered
- Browser-test every combination exhaustively: rejected because most of the behavior is deterministic and cheaper to validate through feature tests.
- Limit validation to visual inspection: rejected because mode resolution and filter workflow are important enough to guard in CI.

View File

@ -1,7 +1,7 @@
# Tasks: Baseline Compare Matrix: High-Density Operator Mode
**Input**: Design documents from `/specs/191-baseline-compare-operator-mode/`
**Prerequisites**: `plan.md`, `spec.md`
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`
**Tests**: Tests are REQUIRED. Extend Pest feature coverage and browser smoke coverage around the existing matrix route.
**Operations**: This feature reuses existing `baseline_compare` run truth only. No new `OperationRun` type, no new run-summary contract, and no new notification channel should be introduced.