## 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
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
BaselineCompareStatsandBaselineCompareSummaryAssessmentinstead of creating a second query-backed summary path. - Introduce one narrow
TenantGovernanceAggregateruntime 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
BaselineCompareLandingkeepBaselineCompareStatsfor 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 contractcontracts/tenant-governance-aggregate.openapi.yaml: internal logical contract for resolving and consuming the aggregate on tenant summary surfacesquickstart.md: focused implementation and verification workflow
Design decisions:
- The shared contract is one derived runtime object, not a new persisted summary or reporting subsystem.
BaselineCompareStatsremains 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 nowconfirmation, 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:
BaselineCompareStatsalready holds most of the needed truth, but no explicit summary contract owns the shared posture family.NeedsAttentionstill re-queries counts already derivable from compare stats, which keeps ownership split across surfaces. - Narrowest correct implementation: Add one derived
TenantGovernanceAggregateplus 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.