TenantAtlas/specs/168-tenant-governance-aggregate-contract/plan.md
ahmido 807d574d31 feat: add tenant governance aggregate contract and action surface follow-ups (#199)
## Summary
- amend the operator UI constitution and related SpecKit templates for the new UI/UX governance rules
- add Spec 168 artifacts plus the tenant governance aggregate implementation used by the tenant dashboard, banner, and baseline compare landing surfaces
- normalize Filament action surfaces around clickable-row inspection, grouped secondary actions, and explicit action-surface declarations across enrolled resources and pages
- fix post-suite regressions in membership cache priming, finding workflow state refresh, tenant review derived-state invalidation, and tenant-bound backup-set related navigation

## Commit Series
- `docs: amend operator UI constitution`
- `spec: add tenant governance aggregate contract`
- `feat: add tenant governance aggregate contract`
- `refactor: normalize filament action surfaces`
- `fix: resolve post-suite state regressions`

## Testing
- `vendor/bin/sail artisan test --compact`
- Result: `3176 passed, 8 skipped (17384 assertions)`

## Notes
- Livewire v4 / Filament v5 stack remains unchanged
- no provider registration changes; `bootstrap/providers.php` remains the relevant location
- no new global-search resources or asset-registration changes in this branch

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #199
2026-03-29 21:14:17 +00:00

24 KiB

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)

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)

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.