TenantAtlas/specs/189-portfolio-triage-review-state/data-model.md
2026-04-10 23:34:02 +02:00

161 lines
9.4 KiB
Markdown

# Data Model: Portfolio Triage Review State and Operator Progress
## Overview
This feature adds one persisted operator-progress entity and a small set of derived read models. Current backup-health and recovery-evidence posture remain authoritative and are not duplicated in storage.
## Existing Source Truths
### Current concern truth
**Type**: Existing derived posture state
**Sources**: `TenantBackupHealthResolver`, `TenantBackupHealthAssessment`, `RestoreSafetyResolver`, `WorkspaceOverviewBuilder`, `TenantResource`, `PortfolioArrivalContextResolver`
| Concern Family | Stable Inputs | Notes |
|---------------|---------------|-------|
| `backup_health` | posture state, stable reason code, schedule-follow-up family, freshness family | Current source of truth for backup triage remains existing tenant backup-health assessment |
| `recovery_evidence` | posture state, stable reason code, restore-evidence concern family | Current source of truth for recovery triage remains existing restore-safety and recovery-evidence assessment |
### Existing arrival context
**Type**: Existing request-scoped continuity contract
**Source**: `PortfolioArrivalContext`, `PortfolioArrivalContextToken`, `PortfolioArrivalContextResolver`
This feature consumes the existing concern-family focus on `/admin/t/{tenant}` to know which triage-review state should be rendered or mutated from the tenant dashboard.
## New Persisted Entity
### TenantTriageReview
**Table**: `tenant_triage_reviews`
**Type**: Workspace-shared persisted operator-progress record
**Lifecycle**: Active while it is the current manual review record for one workspace, tenant, and concern family; becomes inactive when superseded or explicitly resolved
| Field | Type | Validation / Notes |
|------|------|--------------------|
| `id` | bigint | Primary key |
| `workspace_id` | foreign key | Required; must reference the active workspace scope |
| `tenant_id` | foreign key | Required; must reference the tenant inside the same workspace |
| `concern_family` | string | Required; allowlisted to `backup_health` or `recovery_evidence` in V1 |
| `current_state` | string | Required; persisted values limited to `reviewed` or `follow_up_needed` |
| `reviewed_at` | timestamp | Required for active rows; when the manual state was recorded |
| `reviewed_by_user_id` | foreign key nullable | Optional actor reference for workspace-shared progress visibility |
| `review_fingerprint` | string | Required; deterministic fingerprint of the current material concern situation at review time |
| `review_snapshot` | jsonb | Required; bounded diagnostic snapshot with stable concern state, reason, and small supporting keys |
| `last_seen_matching_at` | timestamp nullable | Optional lightweight diagnostic field; initialized on write and may be refreshed opportunistically by future maintenance, but render-time correctness must not depend on it |
| `resolved_at` | timestamp nullable | Null for the current active row; set when a row is superseded or explicitly marked inactive |
| `created_at` | timestamp | Laravel default |
| `updated_at` | timestamp | Laravel default |
### Constraints and Indexes
| Constraint | Purpose |
|-----------|---------|
| Foreign keys on `workspace_id`, `tenant_id`, `reviewed_by_user_id` | Preserve workspace and tenant ownership plus reviewer linkage |
| Partial unique index on (`workspace_id`, `tenant_id`, `concern_family`) where `resolved_at IS NULL` | Ensures at most one active review record per workspace, tenant, and concern family |
| Lookup index on (`workspace_id`, `concern_family`, `resolved_at`, `tenant_id`) | Supports batch loading for workspace and registry current-set resolution |
| Check constraint or enum cast for `current_state` | Limits persisted manual states to `reviewed` or `follow_up_needed` |
## New Derived Read Models
### Derived review state
**Type**: Request-scoped resolved state
**Source**: `TenantTriageReviewStateResolver`
| Derived State | Rule | Stored? |
|--------------|------|---------|
| `not_reviewed` | Current concern exists and no active review row exists for the same workspace, tenant, and concern family | No |
| `reviewed` | Current concern exists, active review row exists, `current_state = reviewed`, and fingerprint matches | No |
| `follow_up_needed` | Current concern exists, active review row exists, `current_state = follow_up_needed`, and fingerprint matches | No |
| `changed_since_review` | Current concern exists, active review row exists, and current fingerprint differs from the stored fingerprint | No |
| `inactive` / excluded | Current concern does not exist in the current affected set | No |
### Current-set progress summary
**Type**: Request-scoped aggregate summary
**Source**: `TenantTriageReviewStateResolver` batch output, consumed by workspace overview and registry surfaces
| Field | Type | Notes |
|------|------|-------|
| `concern_family` | string | `backup_health` or `recovery_evidence` |
| `affected_total` | integer | Count of currently visible affected tenants in the family-specific set |
| `reviewed_count` | integer | Count of current affected rows resolved to `reviewed` |
| `follow_up_needed_count` | integer | Count of current affected rows resolved to `follow_up_needed` |
| `changed_since_review_count` | integer | Count of current affected rows resolved to `changed_since_review` |
| `not_reviewed_count` | integer | Count of current affected rows resolved to `not_reviewed` |
### Resolved row payload
**Type**: Request-scoped row-level bundle
**Source**: `TenantTriageReviewStateResolver`
| Field | Type | Notes |
|------|------|-------|
| `tenant_id` | integer | Tenant identifier for the current row |
| `concern_family` | string | Family the resolved review state refers to |
| `current_concern_present` | boolean | False rows are excluded from current-set progress |
| `current_fingerprint` | string | Deterministic fingerprint of current concern truth |
| `derived_state` | string | `not_reviewed`, `reviewed`, `follow_up_needed`, or `changed_since_review` |
| `reviewed_at` | timestamp or null | From active review row when present |
| `reviewed_by_user_id` | integer or null | From active review row when present |
| `review_snapshot` | array or null | Bounded snapshot for optional secondary display |
## Validation Rules
### Concern-family rules
| Concern Family | Allowed Current States | Fingerprint Inputs |
|---------------|------------------------|--------------------|
| `backup_health` | `reviewed`, `follow_up_needed` | Stable backup posture, stable reason code, schedule-follow-up family, freshness family |
| `recovery_evidence` | `reviewed`, `follow_up_needed` | Stable recovery posture, stable reason code, restore-evidence concern family |
### Snapshot rules
- `review_snapshot` must remain lightweight and bounded.
- Allowed snapshot keys may include `concern_family`, `concern_state`, `reason_code`, `severity_key`, `supporting_key`, and small label-safe metadata.
- Snapshot data must not contain comments, evidence payloads, free-text notes, rendered HTML, or volatile timestamps that would destabilize equality.
### Fingerprint rules
- Fingerprints must be deterministic across repeated reads of the same material concern situation.
- Fingerprints must ignore translated copy, badge labels, rendered descriptions, and volatile time values.
- Fingerprints must change when material concern family, stable state, or stable reason keys change.
## Lifecycle and State Transitions
### Manual mutation transitions
| Event | Existing Active Row | Result |
|------|---------------------|--------|
| `Mark reviewed` | none | Insert new active row with `current_state = reviewed` |
| `Mark reviewed` | active row exists | Set prior row `resolved_at`, then insert new active `reviewed` row |
| `Mark follow_up_needed` | none | Insert new active row with `current_state = follow_up_needed` |
| `Mark follow_up_needed` | active row exists | Set prior row `resolved_at`, then insert new active `follow_up_needed` row |
### Derived-state precedence
1. If the current concern is absent, the row is excluded from current-set state.
2. If the current concern exists and no active row exists, the derived state is `not_reviewed`.
3. If an active row exists and the current fingerprint does not match, the derived state is `changed_since_review`.
4. If an active row exists and fingerprints match, the derived state follows the stored manual state.
### Inactivity handling
- Superseded writes always resolve the previous active row.
- UI correctness does not depend on immediately writing `resolved_at` when a concern naturally disappears from the current affected set; current-set exclusion is derived from current concern truth.
- If later cleanup or maintenance chooses to mark concern-gone rows as resolved, that is an implementation detail and not required for V1 correctness.
## Relationships
- One workspace has many `TenantTriageReview` rows.
- One tenant has many `TenantTriageReview` rows across concern families and historical supersessions.
- One user may review many rows through `reviewed_by_user_id`.
- One current concern family on one tenant resolves to zero or one active row.
## Rendering Rules
- Posture truth remains primary and is displayed independently of review state.
- Registry and overview counts include only current affected rows, never calm or resolved rows.
- Mixed-family registry views must label which concern family the displayed review state refers to.
- Tenant dashboard review-state actions render only when portfolio-arrival context provides a valid concern-family focus.