TenantAtlas/specs/189-portfolio-triage-review-state/plan.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

27 KiB

Implementation Plan: Portfolio Triage Review State and Operator Progress

Branch: 189-portfolio-triage-review-state | Date: 2026-04-10 | Spec: /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/189-portfolio-triage-review-state/spec.md Input: Feature specification from /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/189-portfolio-triage-review-state/spec.md

Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.

Summary

Add one lightweight persisted triage-review record per workspace, tenant, and concern family so existing workspace overview, tenant registry triage, and tenant dashboard arrival surfaces can show operator progress without changing posture truth. The implementation will reuse existing backup-health and recovery-evidence resolvers, existing portfolio-arrival context, existing BadgeCatalog and UiEnforcement patterns, and one batch-loaded review-state resolver so the feature stays narrow, query-bounded, RBAC-safe, and clearly separate from formal TenantReview and ReviewPack workflows.

Technical Context

Language/Version: PHP 8.4.15
Primary Dependencies: Laravel 12, Filament v5, Livewire v4, Pest v4, existing WorkspaceOverviewBuilder, TenantResource, TenantDashboard, PortfolioArrivalContext, TenantBackupHealthResolver, RestoreSafetyResolver, BadgeCatalog, UiEnforcement, and AuditRecorder patterns
Storage: PostgreSQL via Laravel Eloquent with one new table tenant_triage_reviews and no new external caches or background stores
Testing: Pest 4 unit and feature tests, Filament or Livewire surface tests, focused RBAC regressions, run through Laravel Sail
Target Platform: Laravel monolith web application in Sail locally and containerized Linux deployment in staging and production Project Type: web application
Performance Goals: Keep review-state mutations DB-only and under the no-OperationRun threshold, batch-load review state for tenant-registry and workspace-overview affected sets, reuse existing posture batch resolvers, and avoid new N+1 patterns on /admin, /admin/tenants, or /admin/t/{tenant}
Constraints: Preserve posture truth as primary; keep review state workspace-shared and concern-family-specific; no Graph calls; no OperationRun; no new formal review or workflow engine; no notes, assignments, or SLA logic; RBAC must keep non-members at 404 and members without capability at 403; mutations must communicate TenantPilot only scope, show a bounded pre-execution preview plus explicit confirmation, and remain reversible
Scale/Scope: One new persisted model and migration, one small fingerprint helper, one batch resolver, one narrow mutation service, one new badge domain, and additive changes across three existing operator surfaces and two current concern families (backup_health, recovery_evidence)

Constitution Check

GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.

Principle Pre-Research Post-Design Notes
Inventory-first / snapshots-second PASS PASS Backup-health and recovery-evidence remain the source of current concern truth; the new table stores only operator progress truth.
Read/write separation PASS PASS The feature adds only TenantPilot-internal progress writes. Each mutation stays DB-only and sub-2-second, but still uses a bounded pre-execution preview plus explicit confirmation, server-side authorization, mutation-scope copy, audit logging, and focused tests.
Graph contract path N/A N/A No Microsoft Graph calls or contract-registry changes are required.
Deterministic capabilities PASS PASS A new capability constant can be added to the canonical registry and enforced through existing capability resolvers and UI helpers.
Workspace + tenant isolation PASS PASS Review-state reads and writes are always bound to the active workspace and tenant scope.
RBAC-UX authorization semantics PASS PASS Non-members remain 404; in-scope members without mutation capability receive 403; server-side authorization remains authoritative.
Run observability / Ops-UX PASS PASS Review-state mutations are DB-only and below the OperationRun threshold; no remote or queued work is introduced.
Data minimization PASS PASS The persisted snapshot stays bounded to stable concern state, reason, and small diagnostic keys; no notes or rich evidence are stored.
Proportionality / no premature abstraction PASS WITH JUSTIFIED ABSTRACTION PASS WITH JUSTIFIED ABSTRACTION One new persisted model plus one fingerprint and state resolver are justified because request-scoped posture truth cannot persist operator progress across time. No broader workflow framework is added.
Persisted truth / behavioral state PASS PASS The new table represents independent product truth with its own lifecycle. Only behaviorally meaningful manual states are persisted; not_reviewed and changed_since_review remain derived.
UI semantics / few layers PASS PASS The design adds one narrow review-state semantic family and reuses existing domain truth and central badge mappings rather than introducing a presentation framework.
Filament v5 / Livewire v4 compliance PASS PASS The plan stays inside existing Filament v5 and Livewire v4 conventions.
Provider registration location PASS PASS No panel or provider changes are required. Provider registration remains in bootstrap/providers.php.
Global search hard rule PASS PASS No new globally searchable resource is introduced. Existing impacted resources already satisfy the rule: TenantResource has a view page and tenant drilldown pages already exist.
Destructive action safety PASS PASS No destructive action is added. Mark reviewed and Mark follow-up needed are reversible TenantPilot-only progress writes, not destructive mutations.
Asset strategy PASS PASS No new assets are introduced. Existing filament:assets deployment behavior remains unchanged.
Filament-native UI / Action Surface Contract PASS PASS The tenant registry keeps one primary inspect model with review-state mutations in overflow, and the tenant dashboard continuity block remains additive.
Filament UX-001 PASS PASS No create or edit layout changes are introduced; list filters, badges, and continuity sections remain within existing page structures.
Testing truth (TEST-TRUTH-001) PASS PASS The plan adds focused tests for business consequences: persistence, derivation, stale detection, counts, and authorization, not thin presentation layers alone.

Phase 0 Research

Research outcomes are captured in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/189-portfolio-triage-review-state/research.md.

Key decisions:

  • Persist triage-review state in a dedicated tenant_triage_reviews table instead of reusing TenantReview or ReviewPack governance artifacts.
  • Store only the manual states reviewed and follow_up_needed; derive not_reviewed and changed_since_review at read time.
  • Use one active row per workspace, tenant, and concern family, resolving prior rows on superseding writes instead of overwriting them in place.
  • Compute stable concern fingerprints from existing backup-health and recovery-evidence resolver outputs rather than inventing a second concern model.
  • Batch-load active review records alongside existing posture batch resolvers so /admin and /admin/tenants stay query-bounded.
  • Reuse TenantResource::portfolioConcernPriority() as the canonical highest-priority selector whenever a mixed registry slice needs one review-state family to render or mutate.
  • Use a bounded pre-execution preview and explicit confirmation on dashboard and registry mutation actions instead of introducing a separate remote dry-run pipeline for this local DB write.
  • Reuse existing BadgeCatalog, UiEnforcement, PortfolioArrivalContext, and tenant triage fixtures so the UI stays consistent with recent portfolio-triage specs.
  • Record lightweight audit entries for progress mutations without introducing an operator-facing audit timeline.

Phase 1 Design

Design artifacts are created under /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/189-portfolio-triage-review-state/:

  • data-model.md: persisted triage-review entity, derived state precedence, fingerprint rules, and progress-summary read model
  • contracts/portfolio-triage-review-state.logical.openapi.yaml: internal logical contract for current-set summaries, registry review-state rendering, and triage-review mutations
  • quickstart.md: implementation and verification workflow

Design decisions:

  • The new table stores only independent operator-progress truth. Current concern posture remains sourced from TenantBackupHealthResolver and RestoreSafetyResolver.
  • The mutation path uses one narrow service that resolves any prior active record, inserts the new active record, writes one bounded audit event, and is invoked only after a surface-level preview plus explicit confirmation.
  • Review-state resolution is batch-based and concern-family-aware. The resolver never merges backup-health and recovery-evidence into a single global tenant review state, and mixed registry slices reuse the existing TenantResource::portfolioConcernPriority() rules when one family must be chosen.
  • Workspace progress summaries consume the same batch resolver output as tenant-registry rows so counts and badges are derived from one source.
  • The tenant dashboard arrival continuity block remains the only inline mutation surface; registry mutations stay in overflow to preserve the existing action-surface contract, and generic tenant browsing sessions suppress queue-like triage-review mutation language entirely.

Project Structure

Documentation (this feature)

specs/189-portfolio-triage-review-state/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── portfolio-triage-review-state.logical.openapi.yaml
├── checklists/
│   └── requirements.md
└── tasks.md

Source Code (repository root)

apps/platform/
├── app/
│   ├── Filament/
│   │   ├── Pages/
│   │   │   ├── TenantDashboard.php
│   │   │   └── WorkspaceOverview.php
│   │   ├── Resources/
│   │   │   ├── TenantResource.php
│   │   │   └── TenantResource/
│   │   │       └── Pages/ListTenants.php
│   │   └── Widgets/
│   │       ├── Workspace/
│   │       │   ├── WorkspaceNeedsAttention.php
│   │       │   └── WorkspaceSummaryStats.php
│   │       └── Tenant/
│   │           └── TenantTriageArrivalContinuity.php
│   ├── Models/
│   │   └── TenantTriageReview.php
│   ├── Services/
│   │   ├── Audit/AuditRecorder.php
│   │   └── PortfolioTriage/TenantTriageReviewService.php
│   ├── Support/
│   │   ├── Audit/AuditActionId.php
│   │   ├── Auth/Capabilities.php
│   │   ├── Badges/
│   │   │   ├── BadgeDomain.php
│   │   │   └── Domains/TenantTriageReviewStateBadge.php
│   │   ├── PortfolioTriage/
│   │   │   ├── PortfolioArrivalContext.php
│   │   │   ├── PortfolioArrivalContextResolver.php
│   │   │   ├── TenantTriageReviewFingerprint.php
│   │   │   └── TenantTriageReviewStateResolver.php
│   │   ├── Rbac/UiEnforcement.php
│   │   └── Workspaces/WorkspaceOverviewBuilder.php
│   └── database/
│       ├── factories/TenantTriageReviewFactory.php
│       └── migrations/*_create_tenant_triage_reviews_table.php
└── tests/
    ├── Feature/
    │   ├── Concerns/BuildsPortfolioTriageFixtures.php
    │   ├── Filament/
    │   │   ├── TenantDashboardArrivalContextTest.php
    │   │   ├── TenantRegistryRecoveryTriageTest.php
    │   │   ├── TenantRegistryTriageReviewStateTest.php
    │   │   └── WorkspaceOverviewTriageReviewProgressTest.php
    │   └── Rbac/
    │       └── TriageReviewStateAuthorizationTest.php
    └── Unit/
        └── Support/PortfolioTriage/
            ├── TenantTriageReviewFingerprintTest.php
            └── TenantTriageReviewStateResolverTest.php

Structure Decision: Keep the existing Laravel monolith layout under apps/platform. Add one narrow persisted model and migration, one small service plus resolver and fingerprint helper in the existing PortfolioTriage namespace, one badge domain, and additive tests beside the already-established portfolio-triage suites instead of creating a broader workflow subsystem or new base directories.

Implementation Strategy

Phase A — Persist Lightweight Triage-Review Truth

Goal: Introduce the minimal table and model needed to persist workspace-shared operator progress.

Step File Change
A.1 apps/platform/database/migrations/*_create_tenant_triage_reviews_table.php Add the tenant_triage_reviews table with workspace_id, tenant_id, concern_family, current_state, reviewed_at, reviewed_by_user_id, review_fingerprint, review_snapshot, last_seen_matching_at, resolved_at, timestamps, foreign keys, and a partial unique index for active rows in PostgreSQL
A.2 apps/platform/app/Models/TenantTriageReview.php Add the Eloquent model, casts, relationships to workspace, tenant, and reviewing user, plus active or resolved query scopes
A.3 apps/platform/database/factories/TenantTriageReviewFactory.php Add factory states for reviewed, follow_up_needed, active, resolved, and changed-fingerprint scenarios used by feature and unit tests

Phase B — Resolve Stable Fingerprints And Derived Review State

Goal: Combine current concern truth with active review records without creating per-row query fanout or a second concern model.

Step File Change
B.1 apps/platform/app/Support/PortfolioTriage/TenantTriageReviewFingerprint.php Add deterministic fingerprint generation for backup_health and recovery_evidence from stable resolver outputs and concern-family-safe reason keys
B.2 apps/platform/app/Support/PortfolioTriage/TenantTriageReviewStateResolver.php Add batch loading for active review rows keyed by workspace, tenant, and concern family; derive not_reviewed, reviewed, follow_up_needed, and changed_since_review; expose current-set progress counts
B.3 Existing batch posture resolvers and portfolio helpers Reuse TenantBackupHealthResolver, RestoreSafetyResolver, and PortfolioArrivalContextResolver outputs as the sole inputs to fingerprinting and family focus rather than duplicating concern logic

Phase C — Add A Narrow Mutation Service With Audit And RBAC

Goal: Provide one canonical write path for Mark reviewed and Mark follow-up needed.

Step File Change
C.1 apps/platform/app/Services/PortfolioTriage/TenantTriageReviewService.php Add service methods to resolve any prior active row, insert a new active row, bound the snapshot payload, and return the new derived state
C.2 apps/platform/app/Support/Auth/Capabilities.php Add one canonical capability for triage-review mutation, scoped to tenant or workspace triage usage according to existing registry patterns
C.3 apps/platform/app/Support/Audit/AuditActionId.php and apps/platform/app/Services/Audit/AuditRecorder.php usage Add lightweight audit action IDs and record bounded audit entries for the two mutation verbs without adding a new operator-facing audit timeline
C.4 apps/platform/app/Support/Rbac/UiEnforcement.php integration points Reuse visible-but-disabled RBAC gating for members without capability and preserve server-side 404 or 403 semantics on action execution
C.5 apps/platform/app/Filament/Resources/TenantResource.php, apps/platform/app/Filament/Widgets/Tenant/TenantTriageArrivalContinuity.php, and apps/platform/resources/views/filament/widgets/tenant/triage-arrival-continuity.blade.php Add a bounded pre-execution preview and explicit confirmation showing concern family, current review state, target manual state, and TenantPilot only scope before invoking the mutation service

Phase D — Bind Review State Into Existing Operator Surfaces

Goal: Show current review state and progress exactly where portfolio triage already lives.

Step File Change
D.1 apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php Extend current affected-set summary building so overview widgets receive review-state progress counts derived from the same visible tenant population
D.2 apps/platform/app/Filament/Widgets/Workspace/WorkspaceSummaryStats.php and apps/platform/app/Filament/Widgets/Workspace/WorkspaceNeedsAttention.php Render additive current-set progress chips or summary text without changing existing posture truth or drilldown semantics
D.3 apps/platform/app/Filament/Resources/TenantResource.php and apps/platform/app/Filament/Resources/TenantResource/Pages/ListTenants.php Add a review-state column, all four review-state filters, and overflow actions for Mark reviewed and Mark follow-up needed, keeping full-row click and the existing openTenant shortcut intact and reusing TenantResource::portfolioConcernPriority() for mixed-slice family selection
D.4 apps/platform/app/Filament/Widgets/Tenant/TenantTriageArrivalContinuity.php and apps/platform/app/Filament/Pages/TenantDashboard.php Show the resolved review state for the current arrival concern family, expose the same two preview-confirmed mutation actions inside the existing continuity block only when valid triage context exists, and suppress queue-like review-state mutation language in generic tenant browsing sessions
D.5 apps/platform/app/Support/Badges/BadgeDomain.php and apps/platform/app/Support/Badges/Domains/TenantTriageReviewStateBadge.php Register centralized badge semantics for Not reviewed, Reviewed, Follow-up needed, and Changed since review so the same labels render consistently across overview, registry, and dashboard

Phase E — Regression Protection And Verification

Goal: Prove persistence, derivation, and authorization without regressing portfolio-triage semantics.

Step File Change
E.1 apps/platform/tests/Feature/Concerns/BuildsPortfolioTriageFixtures.php Extend existing fixtures with review-state records and changed-fingerprint scenarios so new tests reuse the current portfolio-triage setup
E.2 apps/platform/tests/Unit/Support/PortfolioTriage/TenantTriageReviewFingerprintTest.php Cover stable fingerprint generation, ignored volatile fields, and concern-family separation
E.3 apps/platform/tests/Unit/Support/PortfolioTriage/TenantTriageReviewStateResolverTest.php Cover state precedence, current-set exclusion, batch derivation, and changed-since-review behavior
E.4 apps/platform/tests/Feature/Filament/TenantRegistryTriageReviewStateTest.php and apps/platform/tests/Feature/Filament/TenantRegistryRecoveryTriageTest.php Cover registry review-state badges, filters, overflow actions, and no-conflation with posture truth
E.5 apps/platform/tests/Feature/Filament/TenantDashboardArrivalContextTest.php and apps/platform/tests/Feature/Filament/WorkspaceOverviewTriageReviewProgressTest.php Cover dashboard continuity actions, overview progress counts, current-set-only semantics, and changed-since-review visibility
E.6 apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php Cover non-member 404, member-without-capability 403, visible-but-disabled UI, and allowed mutation success
E.7 Focused Sail and Pint runs Run the minimal verification pack for unit, feature, and RBAC suites plus pint --dirty --format agent

Key Design Decisions

D-001 — Persist only manual review intent, derive everything else

The table stores only reviewed and follow_up_needed. not_reviewed and changed_since_review remain derived from the presence of an active row plus fingerprint comparison. This keeps the persisted state family narrow and behaviorally meaningful.

D-002 — Reuse current concern truth; do not create a second concern model

TenantBackupHealthResolver, RestoreSafetyResolver, and existing portfolio-arrival context already know which family and state currently matter. Fingerprints must be built from those stable outputs, not from new parallel concern DTOs.

D-003 — Keep one active row per workspace, tenant, and concern family

Superseding writes resolve the previous active row and insert a new one. This preserves lightweight continuity without requiring a formal review timeline or a collaboration engine.

D-004 — Use existing portfolio-triage surfaces instead of adding a queue page

Workspace overview, tenant registry, and the tenant dashboard continuity block already form the operator flow. Adding a separate triage-review page would add IA weight without solving a new problem.

D-005 — Use central badge and RBAC helpers, not local status language or ad hoc authorization

The review-state labels must be rendered through the same badge infrastructure used elsewhere, and mutation actions must be gated through the canonical capability registry plus UiEnforcement so the feature does not scatter raw strings or local auth logic.

D-006 — Keep the feature lightweight but still auditable

No OperationRun, no external writes, and no audit timeline are needed, but the two progress mutations still use bounded preview-and-confirmation UI and emit bounded AuditLog entries so the write path remains traceable under the constitution's write-safety rules.

Risk Assessment

Risk Impact Likelihood Mitigation
Fingerprint rules are too sensitive and overproduce Changed since review High Medium Restrict fingerprint inputs to stable state, reason-code, and posture keys; cover false-positive cases in unit tests.
Fingerprint rules are too weak and leave stale Reviewed badges visible High Medium Define family-specific stable keys from existing resolvers and cover state-transition cases in resolver tests.
Review-state loading introduces N+1 behavior on /admin or /admin/tenants High Medium Batch-load active rows keyed by tenant and concern family, reuse current posture batch resolvers, and add query-shape regressions.
Backup and recovery review state become conflated on mixed rows High Medium Keep explicit concern-family focus in resolver output and registry rendering; add family-separation tests.
Lightweight writes are mistaken for formal review completion Medium Medium Keep vocabulary narrow (Review state, Mark reviewed, Changed since review), render posture truth separately, and avoid formal review nouns.
Audit logging grows into a new workflow surface Low Low Limit audit to backend AuditLog recording only and explicitly keep timeline UI and governance artifacts out of scope.

Test Strategy

  • Add focused unit tests for fingerprint generation, ignored volatile inputs, active-row precedence, current-set exclusion, and concern-family isolation.
  • Extend portfolio-triage fixture builders so review-state scenarios can be exercised alongside existing backup and recovery posture scenarios.
  • Add Filament feature coverage for tenant-registry column rendering, all four review-state filters, mixed-family highest-priority selection, preview-confirmed overflow actions, and no-conflation with backup or recovery truth.
  • Extend tenant-dashboard arrival-context tests so the continuity block shows review state, executes the two allowed mutations only when triage context exists, and stays suppressed for generic tenant browsing sessions.
  • Add workspace-overview progress-summary tests so counts are derived only from the current visible affected set and remain separate per concern family.
  • Add RBAC tests for non-member 404, member-without-capability 403, and successful mutation for an authorized operator.
  • Add audit assertions for bounded AuditLog entries on both mutation verbs without requiring an operator-facing timeline.
  • No new relation managers or destructive flows are introduced. Covered Filament surfaces are WorkspaceOverview, WorkspaceSummaryStats, WorkspaceNeedsAttention, TenantResource, ListTenants, TenantDashboard, and TenantTriageArrivalContinuity.
  • Run the minimum focused Sail pack before implementation sign-off: migration-aware unit tests, new and extended Filament feature tests, RBAC tests, and cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent.

Complexity Tracking

Violation Why Needed Simpler Alternative Rejected Because
New persisted tenant_triage_reviews table Request-scoped posture and arrival context cannot preserve operator progress across sessions or team handoff Recomputing from current posture alone cannot answer whether someone already reviewed the current concern
New fingerprint helper and batch resolver The feature must compare current concern truth to the last reviewed concern without N+1 behavior and without duplicating posture logic Ad hoc per-surface comparisons would repeat concern logic, drift across overview or registry surfaces, and increase query fanout
Bounded preview and confirmation for DB-only review-state writes The constitution still requires write safety even though this mutation is a reversible TenantPilot-only local write A blind click would violate the constitution, while a fuller remote dry-run pipeline would overfit a sub-2-second DB-only action

Proportionality Review

  • Current operator problem: Operators can already identify affected tenants and open them with context, but they still cannot track what has been checked, what still needs follow-up, or what became relevant again after a prior review.
  • Existing structure is insufficient because: Existing workspace overview, tenant registry, and arrival context are all read-first and request-scoped. None of them persists lightweight review progress across time.
  • Narrowest correct implementation: Add one lightweight persisted triage-review table, one stable fingerprint helper, one batch resolver, and additive bindings into existing overview, registry, and dashboard surfaces.
  • Ownership cost created: One migration, one new model and factory, one small service plus resolver, one badge domain, small action bindings, and focused regression coverage for persistence, derivation, authorization, and progress counts.
  • Alternative intentionally rejected: Reusing TenantReview or ReviewPack, adding notes or assignments, or introducing a full workflow queue were rejected because they create governance and collaboration weight beyond the current operational-triage need.
  • Release truth: Current-release truth. The feature closes an active workflow gap in the shipped portfolio-triage experience rather than preparing a future governance layer.