# Phase 0 Research: Tenant Governance Aggregate Contract ## Decision: Build the shared contract from `BaselineCompareStats` and its summary assessment instead of creating a second query-backed summary path **Rationale**: `BaselineCompareStats::forTenant()` already resolves baseline assignment, consumable snapshot availability, latest compare-run posture, findings visibility counts, and the governance-attention count family that this spec needs. `NeedsAttention` is the main surface still re-querying overdue, expiring, lapsed, and high-severity finding counts locally even though those values are already derivable from compare stats. Building the aggregate from `BaselineCompareStats` keeps one query-backed truth and avoids a second summary service that would drift. **Alternatives considered**: - Create a new persisted tenant-governance summary record: rejected because the current-release problem is request-time consistency, not independent lifecycle or historical reporting truth. - Create a new query service that bypasses `BaselineCompareStats`: rejected because it would duplicate compare availability and attention logic that already exists. ## Decision: Introduce one narrow `TenantGovernanceAggregate` runtime contract instead of continuing to treat `BaselineCompareStats` as an implicit shared summary **Rationale**: `BaselineCompareStats` is a broad compare-detail object that also carries diagnostics and landing-specific support data. The operator problem is narrower: multiple surfaces need one shared summary posture, count family, and next-action intent. A dedicated aggregate keeps that summary ownership explicit while allowing the landing page to keep deeper diagnostics in `BaselineCompareStats`. **Alternatives considered**: - Use `BaselineCompareStats` directly everywhere with no new contract: rejected because it would leave summary ownership implicit and make it easier for widgets to keep local business rules. - Move every compare diagnostic into the new aggregate: rejected because it would bloat a summary contract into a second detail model. ## Decision: Reuse the existing request-scoped derived-state infrastructure from Spec 167 **Rationale**: The repo already binds `RequestScopedDerivedStateStore` in the Laravel container and uses it for request-local reuse of `ArtifactTruthPresenter`, `OperationUxPresenter`, and `RelatedNavigationResolver`. This feature needs the same boundary: one request-local derived result per deterministic question, explicit invalidation rules, and a guard path that prevents local ad hoc caches from coming back. **Alternatives considered**: - Add a static cache inside `NeedsAttention`, `BaselineCompareNow`, or `BaselineCompareCoverageBanner`: rejected because Spec 167 explicitly moved the codebase away from that pattern. - Use `Cache::remember()` or another cross-request store: rejected because the spec requires request-local reuse only and stale cross-request posture would be harder to reason about. ## Decision: Keep next-action intent inside the aggregate, but keep final URLs and panel-specific drill-down mapping local to surfaces **Rationale**: The semantic drift problem is not only conflicting counts; it is also conflicting operator follow-up. The aggregate must therefore answer the stable business question “what is the next action target category for this tenant state?” while leaving local surfaces free to map that answer to tenant-panel URLs, run links, or findings links appropriate to their context. **Alternatives considered**: - Store absolute URLs inside the aggregate: rejected because final URLs remain panel-aware and surface-local, and capability-gated destinations should stay local to the consuming surface. - Leave next-action computation local to every widget or page: rejected because next-action drift is part of the problem this spec is supposed to solve. ## Decision: Keep `BaselineCompareLanding` as the home for diagnostics, but move its default-visible posture semantics onto the aggregate **Rationale**: The spec explicitly distinguishes default-visible operator posture from diagnostics-only information. `BaselineCompareLanding` should still own evidence gaps, duplicate-name diagnostics, detailed compare context, and the existing `Compare now` action. What changes is that its primary posture zone should no longer be another local semantic owner. **Alternatives considered**: - Leave the landing page fully outside the aggregate: rejected because the landing page is one of the authoritative summary surfaces listed in the spec. - Collapse the landing page into a pure aggregate view: rejected because operators still need compare diagnostics there. ## Decision: Protect the feature with parity, memoization, and guard tests rather than ad hoc performance scripts **Rationale**: The highest-risk regressions are semantic drift, hidden local re-queries, and request-scope leakage. Focused Pest tests can assert those business truths directly, and the derived-state adoption guard can stop future surfaces from quietly reintroducing local caches or unsupported family access patterns. **Alternatives considered**: - Rely only on query counting or manual profiling: rejected because it would miss operator-visible drift and consumer-adoption regressions. - Treat this as a pure unit-test problem: rejected because the spec is about cross-surface agreement and request behavior, which requires feature-level coverage too.