# Implementation Plan: Tenant Governance Aggregate Contract

**Branch**: `168-tenant-governance-aggregate-contract` | **Date**: 2026-03-28 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/168-tenant-governance-aggregate-contract/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/168-tenant-governance-aggregate-contract/spec.md`

**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.

## Summary

Introduce one explicit, derived, tenant-scoped governance aggregate that turns the existing compare posture and governance-attention counts into a single cross-surface contract. The first implementation slice will build the aggregate from `BaselineCompareStats` and its summary assessment, reuse the request-scoped derived-state infrastructure added in Spec 167, and align the tenant dashboard `Baseline Governance` card, tenant governance banner, and Baseline Compare landing around the same posture family and next-action intent while preserving existing landing action semantics and diagnostics hierarchy. A follow-on slice then moves `NeedsAttention` off its local findings-count ownership so the tenant dashboard can render both governance summary cards from one request-local contract without adding persistence or new mutation behavior.

## Technical Context

**Language/Version**: PHP 8.4.15  
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareStats`, `BaselineCompareSummaryAssessor`, `BaselineCompareLanding`, `BaselineCompareNow`, `NeedsAttention`, `BaselineCompareCoverageBanner`, and `RequestScopedDerivedStateStore` from Spec 167  
**Storage**: PostgreSQL unchanged; no new persistence, cache store, or durable summary artifact  
**Testing**: Pest 4 unit and feature tests, including Livewire component coverage and derived-state guard tests, run through Laravel Sail  
**Target Platform**: Laravel monolith web application in Sail locally and containerized Linux deployment in staging/production  
**Project Type**: web application  
**Performance Goals**: One governance-aggregate resolution per request for the same tenant and summary scope; no duplicate aggregate-owned findings queries across the tenant dashboard render; covered summary surfaces keep DB-only render behavior and reuse one stable posture family  
**Constraints**: Derived-only implementation, no new Graph calls, no cross-request cache, no new mutation surfaces, existing `Compare now` confirmation and authorization remain unchanged, diagnostics stay secondary, and no cross-tenant or cross-workspace summary leakage is allowed  
**Scale/Scope**: One tenant at a time, with phased adoption across four tenant-facing summary consumers: MVP parity for `BaselineCompareNow`, `BaselineCompareCoverageBanner`, and `BaselineCompareLanding`, followed by `NeedsAttention` adoption for multi-card request stability

## 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 aggregate is derived from current findings, exception validity, baseline assignment, snapshot availability, and compare-run truth; no new source-of-truth path is introduced. |
| Read/write separation | PASS | PASS | The feature changes read-time summary ownership only. Existing `Compare now` remains the only mutation-adjacent action and stays unchanged. |
| Graph contract path | N/A | N/A | No Graph calls, provider contracts, or `config/graph_contracts.php` changes are required. |
| Deterministic capabilities | PASS | PASS | Authorization and drill-down capability enforcement remain existing server-side checks; the aggregate is read-only and tenant-scoped. |
| Workspace + tenant isolation | PASS | PASS | The aggregate is resolved for one tenant at a time and must never outlive the current request or tenant context. |
| RBAC-UX authorization semantics | PASS | PASS | No new permissions, no role-string checks, and no change to 404-for-non-members / 403-for-in-scope-capability-denial semantics. |
| Run observability / Ops-UX | PASS | PASS | No new `OperationRun` type or feedback path. Existing baseline-compare run visibility remains the operational source. |
| Data minimization | PASS | PASS | No persistence is added; reuse remains request-local only. |
| Proportionality / no premature abstraction | PASS WITH JUSTIFIED ABSTRACTION | PASS WITH JUSTIFIED ABSTRACTION | One narrow runtime contract plus one resolver is justified because four surfaces already share the same summary family and `NeedsAttention` still re-queries data already owned elsewhere. |
| Persisted truth / behavioral state | PASS | PASS | No new tables, artifacts, reason-code families, or persisted statuses are introduced. |
| UI semantics / few layers | PASS | PASS | The aggregate stays below the existing surface layouts. It replaces split summary ownership instead of adding a second presentation framework. |
| Badge semantics (BADGE-001) | PASS | PASS | Existing summary tone and badge mapping stay authoritative; the aggregate supplies data, not ad hoc visual semantics. |
| Filament-native UI / Action Surface Contract | PASS | PASS | Widgets and landing page remain native Filament surfaces. No new row, bulk, or destructive actions are introduced. |
| Filament UX-001 | PASS | PASS | No create/edit/view layout redesign is proposed; only existing summary zones change data ownership. |
| Filament v5 / Livewire v4 compliance | PASS | PASS | The design remains inside the existing Filament v5 + Livewire v4 stack with no legacy API introduction. |
| Provider registration location | PASS | PASS | No panel or provider registration changes; Laravel 11+ registration remains in `bootstrap/providers.php`. |
| Global search hard rule | PASS | PASS | No globally searchable resource changes are proposed in this slice. |
| Destructive action safety | PASS | PASS | No new destructive actions are added. The existing Baseline Compare landing action remains `->action(...)->requiresConfirmation()` and capability-gated. |
| Asset strategy | PASS | PASS | No new assets or `filament:assets` deployment changes are required. |
| Testing truth (TEST-TRUTH-001) | PASS | PASS | The plan adds parity, memoization, and tenant-scope safety tests that protect operator-visible truth rather than thin adapters only. |

## Phase 0 Research

Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/168-tenant-governance-aggregate-contract/research.md`.

Key decisions:

- Build the new aggregate from `BaselineCompareStats` and `BaselineCompareSummaryAssessment` instead of creating a second query-backed summary path.
- Introduce one narrow `TenantGovernanceAggregate` runtime contract so summary ownership is explicit without moving landing-page diagnostics into a bloated new object.
- Reuse the existing request-scoped derived-state infrastructure from Spec 167 rather than adding widget-local or resource-local caches.
- Keep next-action intent inside the aggregate but leave final URLs and panel-specific drill-down mapping local to each surface.
- Let `BaselineCompareLanding` keep `BaselineCompareStats` for deep diagnostics while switching its default-visible posture zone to the shared aggregate.
- Protect the feature with focused parity, memoization, and guard tests instead of ad hoc performance scripts.

## Phase 1 Design

Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/168-tenant-governance-aggregate-contract/`:

- `data-model.md`: persistent source truths plus the new derived tenant-governance runtime contract
- `contracts/tenant-governance-aggregate.openapi.yaml`: internal logical contract for resolving and consuming the aggregate on tenant summary surfaces
- `quickstart.md`: focused implementation and verification workflow

Design decisions:

- The shared contract is one derived runtime object, not a new persisted summary or reporting subsystem.
- `BaselineCompareStats` remains the heavy query-backed source for compare posture, findings counts, and landing diagnostics.
- The aggregate owns shared summary semantics only: posture family, count family, and next-action intent.
- Request-local reuse will extend the Spec 167 derived-state contract instead of introducing local static caches in widgets or pages.
- Consumer surfaces will adopt named helper seams so CI can guard against future reintroduction of local findings queries or direct duplicate aggregate resolution.

## Project Structure

### Documentation (this feature)

```text
specs/168-tenant-governance-aggregate-contract/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── tenant-governance-aggregate.openapi.yaml
├── checklists/
│   └── requirements.md
└── tasks.md
```

### Source Code (repository root)

```text
app/
├── Filament/
│   ├── Pages/
│   │   ├── BaselineCompareLanding.php
│   │   └── TenantDashboard.php
│   └── Widgets/
│       ├── Dashboard/
│       │   ├── BaselineCompareNow.php
│       │   └── NeedsAttention.php
│       └── Tenant/
│           └── BaselineCompareCoverageBanner.php
├── Support/
│   ├── Baselines/
│   │   ├── BaselineCompareStats.php
│   │   ├── BaselineCompareSummaryAssessor.php
│   │   ├── BaselineCompareSummaryAssessment.php
│   │   ├── TenantGovernanceAggregate.php
│   │   └── TenantGovernanceAggregateResolver.php
│   └── Ui/
│       └── DerivedState/
│           └── DerivedStateFamily.php

tests/
├── Feature/
│   ├── Baselines/
│   │   ├── BaselineCompareStatsTest.php
│   │   ├── BaselineCompareSummaryAssessmentTest.php
│   │   └── TenantGovernanceAggregateResolverTest.php
│   ├── Filament/
│   │   ├── BaselineCompareSummaryConsistencyTest.php
│   │   ├── BaselineCompareNowWidgetTest.php
│   │   ├── BaselineCompareCoverageBannerTest.php
│   │   ├── BaselineCompareLandingAdminTenantParityTest.php
│   │   ├── BaselineCompareLandingDuplicateNamesBannerTest.php
│   │   ├── BaselineCompareLandingRbacLabelsTest.php
│   │   ├── BaselineCompareLandingStartSurfaceTest.php
│   │   ├── BaselineCompareLandingWhyNoFindingsTest.php
│   │   ├── NeedsAttentionWidgetTest.php
│   │   └── TenantGovernanceAggregateMemoizationTest.php
│   └── Guards/
│       └── DerivedStateConsumerAdoptionGuardTest.php
```

**Structure Decision**: Keep the existing Laravel monolith structure. Add one narrow runtime contract plus resolver under `app/Support/Baselines`, adopt it through the current Filament widgets and landing page, and extend the existing request-scoped derived-state infrastructure instead of creating new base directories or a broader presentation layer.

## Implementation Strategy

### Phase A — Introduce the Aggregate Contract and Resolver

**Goal**: Add one explicit tenant-governance runtime object and one resolver that derives it from existing compare truth.

| Step | File | Change |
|------|------|--------|
| A.1 | `app/Support/Baselines/TenantGovernanceAggregate.php` | Add the derived runtime contract that holds the shared summary posture, count family, and next-action intent for one tenant |
| A.2 | `app/Support/Baselines/TenantGovernanceAggregateResolver.php` | Add the resolver that derives the aggregate from `BaselineCompareStats` and `BaselineCompareSummaryAssessment` without adding persistence |
| A.3 | `app/Support/Baselines/BaselineCompareStats.php` | Expose the aggregate's count family from the existing source path and keep the landing diagnostics sourced from the same stats object |

### Phase B — Reuse the Existing Derived-State Infrastructure

**Goal**: Make repeated reads of the same tenant aggregate request-stable without adding ad hoc widget caches.

| Step | File | Change |
|------|------|--------|
| B.1 | `app/Support/Ui/DerivedState/DerivedStateFamily.php` | Add a dedicated family for tenant-governance aggregate reuse, unless the final implementation can reuse the existing family contract without ambiguity |
| B.2 | `app/Support/Baselines/TenantGovernanceAggregateResolver.php` and `specs/167-derived-state-memoization/contracts/request-scoped-derived-state.logical.openapi.yaml` | Route aggregate resolution through the existing request-scoped derived-state store and declare the supported first-slice consumer paths for the new family |
| B.3 | `tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php` | Extend the allowed-family and guarded-consumer validation so first-slice summary surfaces cannot fall back to local caches or direct duplicate findings queries |

### Phase C — Adopt the First Shared Summary Surfaces

**Goal**: Make the tenant dashboard governance card, landing page, and tenant banner consume one shared semantic source before the second dashboard summary surface joins the contract.

| Step | File | Change |
|------|------|--------|
| C.1 | `app/Filament/Widgets/Dashboard/BaselineCompareNow.php` | Replace direct `BaselineCompareStats` summary reads with the aggregate while keeping local URL mapping for findings, run, and landing destinations |
| C.2 | `app/Filament/Widgets/Tenant/BaselineCompareCoverageBanner.php` | Drive banner visibility, tone, and next-action intent from the same aggregate-backed posture family |
| C.3 | `app/Filament/Pages/BaselineCompareLanding.php` | Use the aggregate for the default-visible posture summary while preserving existing compare diagnostics, evidence-gap detail, and `Compare now` behavior |
| C.4 | `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`, `tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php`, `tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php`, and `tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php` | Preserve unchanged compare-start authorization and confirmation semantics while proving diagnostics remain secondary to the aggregate-owned posture zone |

### Phase D — Align Next-Action Intent

**Goal**: Make the first shared summary surfaces point operators toward the same class of next step for the same tenant state.

| Step | File | Change |
|------|------|--------|
| D.1 | `app/Support/Baselines/TenantGovernanceAggregate.php` and `app/Support/Baselines/TenantGovernanceAggregateResolver.php` | Add next-action label and target ownership to the aggregate contract |
| D.2 | `app/Filament/Widgets/Dashboard/BaselineCompareNow.php`, `app/Filament/Widgets/Tenant/BaselineCompareCoverageBanner.php`, and `app/Filament/Pages/BaselineCompareLanding.php` | Map shared next-action intent to local URLs and operator-facing labels without reintroducing local business rules |
| D.3 | `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`, `tests/Feature/Filament/BaselineCompareNowWidgetTest.php`, `tests/Feature/Filament/BaselineCompareCoverageBannerTest.php`, `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`, and `tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php` | Prove next-action parity for running, failed, unavailable, open-findings, overdue-without-new-drift, lapsed-without-new-drift, caution, and stale states |

### Phase E — Extend to `NeedsAttention` and Multi-Card Stability

**Goal**: Bring the second dashboard summary surface onto the same request-local contract and remove duplicate findings-count ownership.

| Step | File | Change |
|------|------|--------|
| E.1 | `app/Filament/Widgets/Dashboard/NeedsAttention.php` | Replace local `Finding::query()` ownership for overdue, expiring, lapsed, and high-severity summary counts with the shared aggregate and keep only local operations-in-progress logic |
| E.2 | `tests/Feature/Filament/NeedsAttentionWidgetTest.php`, `tests/Feature/Filament/TenantGovernanceAggregateMemoizationTest.php`, `specs/167-derived-state-memoization/contracts/request-scoped-derived-state.logical.openapi.yaml`, and `tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php` | Prove request-local reuse, no-tenant safety, tenant-switch safety, and extension of the guarded consumer contract to cover removal of hidden local findings-count ownership in `NeedsAttention` |

### Phase F — Regression Protection and Verification

**Goal**: Prove semantic consistency, request reuse, and tenant-scope safety.

| Step | File | Change |
|------|------|--------|
| F.1 | `tests/Feature/Baselines/TenantGovernanceAggregateResolverTest.php` | Add focused resolver coverage for the full FR-168-014 matrix: unavailable prerequisites, in-progress compare, failed compare, open findings requiring action, overdue-without-new-drift, lapsed-without-new-drift, caution, stale, and trustworthy all-clear |
| F.2 | `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`, `tests/Feature/Filament/BaselineCompareNowWidgetTest.php`, `tests/Feature/Filament/BaselineCompareCoverageBannerTest.php`, `tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`, `tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php`, `tests/Feature/Filament/BaselineCompareLandingDuplicateNamesBannerTest.php`, and `tests/Feature/Filament/NeedsAttentionWidgetTest.php` | Assert that covered surfaces share the same count family, posture family, diagnostics hierarchy, and next-action intent for seeded tenant states |
| F.3 | `tests/Feature/Filament/TenantGovernanceAggregateMemoizationTest.php` and `tests/Feature/Guards/DerivedStateConsumerAdoptionGuardTest.php` | Assert that one request stores one derived governance aggregate per tenant scope, no tenant state leaks, and no covered consumer reintroduces local findings-count ownership |
| F.4 | `vendor/bin/sail bin pint --dirty --format agent` and focused Pest runs | Apply formatting and run the smallest verification pack that covers stats, landing authorization and hierarchy, widgets, memoization, and the derived-state guard |

## Key Design Decisions

### D-001 — The new contract is summary-focused and derived from existing compare truth

The shared aggregate must not become a second diagnostics object. `BaselineCompareStats` already owns compare availability, findings counts, and low-level detail. The aggregate will summarize the operator-facing posture family built from that truth.

### D-002 — Request reuse must flow through Spec 167 infrastructure, not local caches

The repo already has `RequestScopedDerivedStateStore` and a guard model for supported families. This feature should extend that path instead of introducing widget-level static arrays or request-attribute caches.

### D-003 — Next-action intent belongs in the aggregate; final URLs stay local

The operator problem includes conflicting next steps, so the aggregate must own whether the next action is “findings”, “run”, “landing”, or “none”. Each surface will continue to map that intent to panel-specific URLs and capability-aware affordances.

### D-004 — Baseline Compare landing keeps diagnostics but loses summary re-ownership

The landing page remains the home for deep compare diagnostics and the compare-start action. It should stop being a separate semantic owner for the default-visible posture zone.

## Risk Assessment

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Aggregate and compare stats drift into parallel truths | High | Medium | Build the aggregate directly from one `BaselineCompareStats` resolution and keep diagnostics sourced from that same object |
| Request-scoped reuse omits tenant or route-sensitive inputs | High | Medium | Keep the aggregate user-agnostic, key it by workspace and tenant scope, and add tenant-switch / no-tenant regression tests |
| `NeedsAttention` keeps hidden local count ownership | High | Medium | Add consumer guard declarations plus tests that cover overdue, lapsed, and expiring states with zero new drift |
| Landing summary and dashboard summary stay semantically misaligned | Medium | Medium | Add cross-surface parity tests that assert one posture family and one next-action intent across dashboard, banner, and landing |
| The aggregate grows into a second presentation framework | Medium | Low | Limit it to summary posture, counts, and next-action intent; leave URLs, layout, and diagnostics local |

## Test Strategy

- Add focused resolver tests that cover the full FR-168-014 matrix: unavailable prerequisites, compare in progress, compare failed, open findings requiring action, overdue-without-new-drift, lapsed governance without new drift, cautionary limited-confidence results, stale results, and trustworthy all-clear results.
- Keep existing Baseline Compare stats and summary-assessment tests as the canonical low-level truth tests.
- Extend Filament widget and landing tests so covered surfaces assert the same headline family, tone, next-action label, preserved drill-down destination continuity, and diagnostics-secondary hierarchy for the same tenant state.
- Preserve existing `Compare now` confirmation, capability gating, and tenant-safe landing authorization behavior through explicit landing regression coverage.
- Add one request-scoped memoization test that renders a page with multiple consumers and proves a single stored governance aggregate exists for that tenant scope.
- Extend the Spec 167 consumer-adoption guard so the new family and consumer surfaces cannot regress back to local ad hoc caches or repeated findings-count queries.
- Preserve Livewire v4-compatible component tests and run the minimum focused Sail verification pack before implementation completion.

## Complexity Tracking

| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| New derived runtime contract plus derived-state family | Four existing summary surfaces already share one operator posture family, and the repo already standardized request-local reuse for similar deterministic derived state | Per-widget hardening would preserve split semantic ownership and allow local findings queries or ad hoc caches to reappear |

## Proportionality Review

- **Current operator problem**: Tenant operators can already see overlapping dashboard cards, banners, and landing summaries that answer the same governance question through different logic paths, especially when no new drift exists but overdue or unhealthy governance still does.
- **Existing structure is insufficient because**: `BaselineCompareStats` already holds most of the needed truth, but no explicit summary contract owns the shared posture family. `NeedsAttention` still re-queries counts already derivable from compare stats, which keeps ownership split across surfaces.
- **Narrowest correct implementation**: Add one derived `TenantGovernanceAggregate` plus one resolver that builds from existing compare truth and reuses the existing request-scoped derived-state store.
- **Ownership cost created**: One new runtime DTO/resolver pair, one derived-state family declaration, one consumer guard extension, and a focused set of parity + memoization tests.
- **Alternative intentionally rejected**: Continuing to harden each widget or page independently was rejected because it would preserve multiple semantic owners and repeated queries. A persisted summary record or cross-request cache was rejected because the current-release need is request-time consistency only.
- **Release truth**: Current-release truth. The affected surfaces are already shipped and already overlap semantically.
