TenantAtlas/specs/189-portfolio-triage-review-state/data-model.md
ahmido 2f45ff5a84 feat: add portfolio triage review state tracking (#220)
## Summary
- add tenant triage review-state persistence, fingerprinting, resolver logic, service layer, and migration for current affected-set tracking
- surface review-state and affected-set progress across tenant registry, tenant dashboard arrival continuity, and workspace overview
- extend RBAC, audit/badge support, specs, and test coverage for portfolio triage review-state workflows
- suppress expected hidden-page background transport failures in the global unhandled rejection logger while keeping visible-page failures logged

## Validation
- targeted Pest coverage added for tenant registry, workspace overview, arrival context, RBAC authorization, badges, fingerprinting, resolver behavior, and logger asset behavior
- code formatted with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Notes
- full suite was not re-run in this final step
- branch includes the spec artifacts under `specs/189-portfolio-triage-review-state/`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #220
2026-04-10 21:35:17 +00:00

9.4 KiB

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.