TenantAtlas/specs/187-portfolio-triage-arrival-context/data-model.md
2026-04-09 23:29:20 +02:00

130 lines
6.7 KiB
Markdown

# Data Model: Portfolio Triage Arrival Context
## Overview
This feature introduces no new persisted tables or stored entities. The model impact is a small set of request-scoped, derived runtime contracts that connect existing workspace triage truth to tenant-dashboard arrival rendering.
## Existing Source Truths
### Workspace overview attention item
**Type**: Existing derived workspace summary payload
**Source**: `WorkspaceOverviewBuilder`
| Field | Type | Notes |
|------|------|-------|
| `tenant_route_key` | string | Tenant route identifier already used in workspace attention items |
| `family` | string | Existing concern family, such as `backup_health` or `recovery_evidence` |
| `title` | string | Operator-facing concern headline |
| `body` | string | Operator-facing bounded summary |
| `supporting_message` | string or null | Existing claim-boundary or next-step phrasing |
| `reason_context.family` | enum | `backup_health` or `recovery_evidence` |
| `reason_context.state` | enum | Existing posture state emitted from workspace triage |
| `reason_context.reason` | string or null | Existing bounded reason code |
| `destination.kind` | string | Existing destination type |
| `destination.url` | string or null | Existing destination URL |
| `destination.disabled` | bool | Existing capability-aware destination availability |
| `destination.helper_text` | string or null | Existing degraded-access explanation |
### Tenant-registry triage state
**Type**: Existing query-driven list state
**Source**: `ListTenants::applyRequestedTriageIntent()`
| Field | Type | Validation |
|------|------|------------|
| `backup_posture` | array<string> | Sanitized through `TenantResource::sanitizeBackupPostures()` |
| `recovery_evidence` | array<string> | Sanitized through `TenantResource::sanitizeRecoveryEvidenceStates()` |
| `triage_sort` | string or null | Sanitized through `TenantResource::sanitizeTriageSort()` |
### Existing destination continuity inputs
**Type**: Existing route-level continuity params
| Surface | Param | Purpose |
|--------|-------|---------|
| Backup-set list | `backup_health_reason` | Explains why backup detail or backup list was opened |
| Restore-run list | `recovery_posture_reason` | Explains why restore history was opened |
| Restore-run detail | `recovery_posture_reason` | Explains why a specific restore run was opened |
## New Derived Runtime Contracts
### PortfolioArrivalContext
**Type**: Request-scoped value object
**Lifecycle**: Decoded from a tenant-dashboard query token, validated against current scope, discarded after the request completes
| Field | Type | Validation |
|------|------|------------|
| `version` | integer | Must match the current token version |
| `sourceSurface` | enum | `workspace_overview` or `tenant_registry` |
| `tenantRouteKey` | string | Must match the current tenant route or resolve to the same tenant |
| `workspaceId` | integer or null | Optional scope binding; when present, must match current workspace context |
| `concernFamily` | enum | `backup_health` or `recovery_evidence` |
| `concernState` | enum | `absent`, `stale`, `degraded`, `unvalidated`, or `weakened` |
| `concernReason` | string or null | Allowlisted per concern family |
| `arrivalSummary` | string | Derived bounded explanation of why the operator arrived |
| `claimBoundary` | string or null | Derived reminder that arrival reason is not a stronger truth than current evidence supports |
| `nextStep` | `NextStepTarget` | Capability-aware navigation target |
| `returnTarget` | `ReturnTarget` | Portfolio return link with bounded route and query state |
| `currentTruthDelta` | string or null | Optional note when current tenant truth differs from the arrival reason |
### NextStepTarget
**Type**: Request-scoped navigation descriptor
| Field | Type | Notes |
|------|------|-------|
| `kind` | enum | `tenant_dashboard`, `backup_sets`, `restore_runs`, `restore_run_detail` |
| `label` | string | Operator-facing next-step verb + object label |
| `url` | string or null | Null when unavailable under current RBAC |
| `disabled` | bool | True when the user can see the arrival block but cannot access the target |
| `helperText` | string or null | Truthful reason the next step cannot be opened |
| `reasonParam` | array<string, scalar> | Existing `backup_health_reason` or `recovery_posture_reason` continuation payload when needed |
### ReturnTarget
**Type**: Request-scoped navigation descriptor
| Field | Type | Notes |
|------|------|-------|
| `kind` | enum | `workspace_overview` or `tenant_registry` |
| `label` | string | Operator-facing return label |
| `url` | string | Canonical return URL |
| `filters` | array<string, mixed> | Allowlisted triage filters and sort state for registry returns |
## Validation Rules
### Concern family and state compatibility
| Concern Family | Allowed States | Allowed Reasons |
|---------------|----------------|-----------------|
| `backup_health` | `absent`, `stale`, `degraded` | Existing `TenantBackupHealthAssessment` reason constants that justify tenant-opening triage |
| `recovery_evidence` | `unvalidated`, `weakened` | Existing restore-safety or recovery-triage reason codes such as `no_history`, `failed`, `partial`, or `completed_with_follow_up` |
### Token decode rules
- Empty, malformed, or unsupported-version tokens decode to `null`.
- Unknown families, states, reasons, or return kinds decode to `null`.
- Tokens bound to a different tenant route or incompatible workspace context decode to `null`.
- Successful decode does not replace current tenant truth; it only authorizes rendering of the continuity block.
### Return filter rules
- Registry return filters may include only the existing triage keys: `backup_posture`, `recovery_evidence`, and `triage_sort`.
- Filter values must pass the same sanitizers used by `ListTenants` on mount.
- Unknown or empty filters are dropped from the return target.
## Relationships
- One workspace attention item may generate one `PortfolioArrivalContext` when its destination is a tenant-level surface.
- One filtered tenant-registry session may generate one `PortfolioArrivalContext` per tenant-open action.
- One `PortfolioArrivalContext` owns exactly one `NextStepTarget` and one `ReturnTarget`.
- One tenant dashboard request may resolve zero or one valid `PortfolioArrivalContext`.
## Rendering Rules
- No valid `PortfolioArrivalContext` means the tenant dashboard renders normally with no continuity block.
- A valid `PortfolioArrivalContext` may render even if current tenant truth has changed, but the block must differentiate arrival reason from current truth.
- `NextStepTarget.disabled = true` still allows the block to render, but the CTA must be absent or disabled with helper text.
- `ReturnTarget` renders only when the origin surface can be reconstituted safely.