# 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.