# Phase 1 Data Model: Tenant Governance Aggregate Contract ## Overview This feature does not add a database table or persisted summary artifact. It formalizes the existing persistent source truths that already drive tenant governance posture and adds one derived runtime contract plus request-scoped reuse rules for the shared summary family. ## Persistent Source Truths ### Tenant **Purpose**: The tenant is the scope boundary for the aggregate and for every covered surface. **Key fields**: - `id` - `workspace_id` - `external_id` **Validation rules**: - Aggregate resolution is allowed only for one explicit tenant scope at a time. - Workspace membership and tenant entitlement remain authoritative before any summary surface renders. ### BaselineTenantAssignment and BaselineProfile **Purpose**: Define whether the tenant has an assigned baseline and which profile and snapshot chain determine compare availability. **Key fields**: - `baseline_tenant_assignments.tenant_id` - `baseline_tenant_assignments.baseline_profile_id` - `baseline_profiles.id` - `baseline_profiles.name` - `baseline_profiles.active_snapshot_id` **Validation rules**: - Missing assignment and missing snapshot remain derived availability states, not new persisted governance states. ### BaselineSnapshot **Purpose**: Supplies consumable compare-snapshot truth for the assigned baseline profile. **Key fields**: - `id` - `baseline_profile_id` - lifecycle/completion fields already used by `BaselineSnapshotTruthResolver` **Validation rules**: - Snapshot usability remains governed by existing compare truth logic. - The aggregate must not invent a second snapshot-availability rule set. ### OperationRun **Purpose**: Supplies baseline-compare progress, completion, failure, and freshness context. **Key fields**: - `id` - `tenant_id` - `workspace_id` - `type` - `status` - `outcome` - `completed_at` - `context` **Validation rules**: - Only existing baseline-compare runs influence compare posture in this slice. - The aggregate does not introduce a new run type or a second operational state model. ### Finding and FindingException **Purpose**: Supply overdue workflow state, visible drift pressure, and accepted-risk governance validity. **Key fields**: - `findings.tenant_id` - `findings.status` - `findings.severity` - `findings.due_at` - `finding_exceptions.current_validity_state` **Validation rules**: - Overdue, expiring, lapsed, active-non-new, and high-severity-active counts remain derived from current findings truth. - The aggregate must not redefine accepted-risk validity semantics. ## Existing Runtime Source Objects ### BaselineCompareStats **Purpose**: Existing query-backed compare truth object that already combines compare availability, diagnostics, and governance-attention counts for one tenant. **Key fields consumed by this feature**: - `state` - `profileName` - `operationRunId` - `findingsCount` - `lastComparedHuman` - `lastComparedIso` - `reasonCode` - `overdueOpenFindingsCount` - `expiringGovernanceCount` - `lapsedGovernanceCount` - `activeNonNewFindingsCount` - `highSeverityActiveFindingsCount` **Relationship to the new aggregate**: - The aggregate is built from one `BaselineCompareStats` resolution. - Landing diagnostics continue to read `BaselineCompareStats` directly. ### BaselineCompareSummaryAssessment **Purpose**: Existing summary interpretation object that maps `BaselineCompareStats` to posture family, tone, headline, supporting message, and next-action target. **Key fields consumed by this feature**: - `stateFamily` - `headline` - `supportingMessage` - `tone` - `positiveClaimAllowed` - `reasonCode` - `nextActionTarget()` - `nextActionLabel()` **Relationship to the new aggregate**: - The aggregate uses the summary assessment as its summary-semantics input. - The feature must not fork a second state-family interpretation path. ## New Derived Runtime Entities ### TenantGovernanceAggregate **Purpose**: One tenant-scoped operator summary contract that owns the shared posture, count family, and next-action intent for tenant-governance summary surfaces. #### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `tenant_id` | int | yes | Tenant scope for the aggregate | | `workspace_id` | int | yes | Workspace scope for request-local reuse safety | | `profile_name` | string nullable | no | Assigned baseline profile name when available | | `compare_state` | string | yes | Existing compare availability or execution state from `BaselineCompareStats` | | `state_family` | string | yes | Existing summary posture family from `BaselineCompareSummaryAssessment` | | `tone` | string | yes | Existing tone family used by covered summary surfaces | | `headline` | string | yes | Operator-facing summary headline | | `supporting_message` | string nullable | no | Secondary operator-facing explanation | | `reason_code` | string nullable | no | Summary reason code when available | | `last_compared_label` | string nullable | no | Human-readable freshness label | | `visible_drift_findings_count` | int | yes | Visible drift findings count from compare stats | | `overdue_open_findings_count` | int | yes | Overdue open findings count | | `expiring_governance_count` | int | yes | Accepted-risk governance nearing expiry | | `lapsed_governance_count` | int | yes | Accepted-risk governance no longer valid | | `active_non_new_findings_count` | int | yes | Active non-new findings pressure | | `high_severity_active_findings_count` | int | yes | High-severity active findings pressure | | `next_action_label` | string | yes | Stable operator-facing next-step label | | `next_action_target` | enum(`findings`,`run`,`landing`,`none`) | yes | Stable next-action intent; surfaces map this to local URLs | | `positive_claim_allowed` | bool | yes | Whether the current summary posture qualifies as a trustworthy all-clear | | `stats` | `BaselineCompareStats` | yes | Embedded source truth used when a consumer also needs compare diagnostics | | `summary_assessment` | `BaselineCompareSummaryAssessment` | yes | Embedded summary truth used by all covered surfaces | #### Validation rules - The aggregate must be built from exactly one `BaselineCompareStats` instance and exactly one `BaselineCompareSummaryAssessment` derived from that stats instance. - Count fields must be copied from the same stats instance; surfaces must not recompute them locally. - Final URLs, local badges, and layout-specific copy remain outside the aggregate. ### TenantGovernanceAggregateResolver **Purpose**: Service seam that resolves one `TenantGovernanceAggregate` per tenant scope and handles request-local reuse. #### Fields / Inputs | Field | Type | Required | Description | |-------|------|----------|-------------| | `tenant` | `Tenant` nullable | yes | Target tenant; nullable only for explicit no-tenant handling paths | | `workspace_id` | int nullable | no | Optional explicit scope input for guardable key composition | | `surface_variant` | string | yes | Stable consumer variant used when request-local reuse or guard declarations need surface-specific context | #### Validation rules - The resolver must stay derived-only. - Reuse must be request-local only. - A no-tenant or wrong-tenant path must never reuse a previous tenant’s aggregate. ### DerivedStateFamily::TenantGovernanceAggregate **Purpose**: Extends the existing Spec 167 request-scoped derived-state contract so the aggregate follows the same consumer-declaration and guard rules as other supported deterministic families. #### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `family` | enum value | yes | Stable family identifier for request-local aggregate reuse | | `default_freshness_policy` | enum | yes | Expected to remain `invalidate_after_mutation` for landing refresh paths | | `allows_negative_result_cache` | bool | yes | Aggregate resolution should be reusable for deterministic no-tenant or unavailable states only if the final key contract explicitly supports it | ## Consumer Mapping | Consumer | Aggregate responsibility | Local responsibility | |---|---|---| | `NeedsAttention` | Overdue, expiring, lapsed, high-severity counts; baseline posture headline; next-action intent | Operations-in-progress count and widget-specific healthy fallback layout | | `BaselineCompareNow` | Baseline posture family, headline, supporting message, next-action intent | Tenant-panel URL mapping for findings, run detail, and landing drill-down | | `BaselineCompareCoverageBanner` | Banner visibility posture family, tone, headline, supporting message, next-action intent | Banner-specific show/hide threshold and local URL mapping | | `BaselineCompareLanding` | Default-visible posture zone and next-action intent | Compare diagnostics, evidence-gap detail, duplicate-name detail, and existing `Compare now` action | ## Derived State Lifecycle 1. A covered tenant summary surface asks the resolver for the current tenant aggregate. 2. The resolver resolves or reuses one request-scoped aggregate for that tenant scope. 3. Covered surfaces read the same posture family, count family, and next-action intent from the shared aggregate. 4. If an in-request mutation or refresh path makes the current aggregate stale, the resolver uses explicit invalidation or fresh-resolution semantics. 5. The next request builds a new aggregate from current source truth. ## Migration Notes - No schema migration is required. - Existing `BaselineCompareStats` tests remain the low-level source-truth tests. - If the implementation adds a new derived-state family, it must also extend the Spec 167 consumer-declaration contract and guard test.