TenantAtlas/specs/222-findings-intake-team-queue/data-model.md
Ahmed Darrazi a2e855bd81
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 49s
feat: add findings intake queue and stabilize follow-up regressions
2026-04-22 00:51:18 +02:00

204 lines
8.6 KiB
Markdown

# Data Model: Findings Intake & Team Queue V1
## Overview
This feature does not add or modify persisted entities. It introduces three derived models:
- the canonical admin-plane `Findings intake` queue at `/admin/findings/intake`
- the fixed `Unassigned` and `Needs triage` queue-view state
- the post-claim handoff result that points the operator into the existing `My Findings` surface
All three remain projections over existing finding, tenant membership, workspace context, and audit truth.
## Existing Persistent Inputs
### 1. Finding
- Purpose: Tenant-owned workflow record representing current governance or remediation work.
- Key persisted fields used by this feature:
- `id`
- `workspace_id`
- `tenant_id`
- `status`
- `severity`
- `due_at`
- `subject_display_name`
- `owner_user_id`
- `assignee_user_id`
- `reopened_at`
- `triaged_at`
- `in_progress_at`
- Relationships used by this feature:
- `tenant()`
- `ownerUser()`
- `assigneeUser()`
Relevant existing semantics:
- `Finding::openStatuses()` defines intake inclusion and intentionally excludes `acknowledged`.
- `Finding::openStatusesForQuery()` remains relevant for `My Findings`, but not for intake.
- Spec 219 defines owner-versus-assignee meaning.
- Spec 221 defines the post-claim destination when a finding becomes assigned to the current user.
### 2. Tenant
- Purpose: Tenant boundary for queue disclosure, claim authorization, and tenant-plane detail drilldown.
- Key persisted fields used by this feature:
- `id`
- `workspace_id`
- `name`
- `external_id`
- `status`
### 3. TenantMembership And Capability Truth
- Purpose: Per-tenant entitlement and capability boundary for queue visibility and claim.
- Sources:
- `tenant_memberships`
- existing `CapabilityResolver`
- Key values used by this feature:
- tenant membership presence
- role-derived `TENANT_FINDINGS_VIEW`
- role-derived `TENANT_FINDINGS_ASSIGN`
Queue disclosure, tab badges, filter options, and claim affordances must only materialize for tenants where the actor remains a member and is authorized for the relevant finding capability.
### 4. Workspace Context
- Purpose: Active workspace selection in the admin plane.
- Source: Existing workspace session context, not a new persisted model for this feature.
- Effect on this feature:
- gates entry into the admin intake page
- constrains visible tenants to the current workspace
- feeds the default active-tenant prefilter through `CanonicalAdminTenantFilterState`
### 5. AuditLog
- Purpose: Existing audit record for security- and workflow-relevant mutations.
- Effect on this feature:
- successful claims write an audit entry through the existing `finding.assigned` action ID
- the audit payload records before/after assignment state, workspace, tenant, actor, and target finding
## Derived Presentation Entities
### 1. IntakeFindingRow
Logical row model for `/admin/findings/intake`.
| Field | Meaning | Source |
|---|---|---|
| `findingId` | Target finding identifier | `Finding.id` |
| `tenantId` | Tenant route scope for detail drilldown | `Finding.tenant_id` |
| `tenantLabel` | Tenant name visible in the queue | `Tenant.name` |
| `summary` | Operator-facing finding summary | `Finding.subject_display_name` plus existing fallback logic |
| `severity` | Severity badge value | `Finding.severity` |
| `status` | Lifecycle badge value | `Finding.status` |
| `dueAt` | Due date if present | `Finding.due_at` |
| `dueState` | Derived urgency label such as overdue or due soon | existing findings due-state logic |
| `ownerLabel` | Accountable owner when present | `ownerUser.name` |
| `intakeReason` | Why the row still belongs in shared intake | derived from lifecycle plus assignment truth |
| `detailUrl` | Tenant finding detail route | derived from tenant finding view route |
| `navigationContext` | Return-path payload back to intake | derived from `CanonicalNavigationContext` |
| `claimEnabled` | Whether the current actor may claim now | derived from assign capability and current claimable state |
Validation rules:
- Row inclusion requires all of the following:
- finding belongs to the current workspace
- finding belongs to a tenant the current user may inspect
- finding status is in `Finding::openStatuses()`
- `assignee_user_id` is `null`
- Already-assigned findings are excluded even if overdue or reopened.
- `acknowledged` findings are excluded.
- Hidden-tenant or capability-blocked findings produce no row, no count, no tab badge contribution, and no tenant filter option.
Derived queue reason:
- `Needs triage` when status is `new` or `reopened`
- `Unassigned` when status is `triaged` or `in_progress`
### 2. FindingsIntakeState
Logical state model for the intake page.
| Field | Meaning |
|---|---|
| `workspaceId` | Current admin workspace scope |
| `queueView` | Fixed queue mode: `unassigned` or `needs_triage` |
| `tenantFilter` | Optional active-tenant prefilter, defaulted from canonical admin tenant context |
| `fixedScope` | Constant indicator that the page remains restricted to unassigned intake rows |
Rules:
- `queueView` is limited to `unassigned` and `needs_triage`.
- `tenantFilter` is clearable; `fixedScope` is not.
- `tenantFilter` values may only reference entitled tenants.
- Invalid or stale tenant filter state is discarded rather than widening visibility.
- Summary counts and tab badges reflect only visible intake rows after the active queue view and tenant prefilter are applied.
### 3. ClaimOutcome
Logical mutation result for `Claim finding`.
| Field | Meaning | Source |
|---|---|---|
| `findingId` | Claimed finding identifier | `Finding.id` |
| `tenantId` | Tenant scope of the claimed finding | `Finding.tenant_id` |
| `assigneeUserId` | New assignee after success | current user ID |
| `auditActionId` | Stable audit action identifier | existing `finding.assigned` |
| `nextPrimaryAction` | Primary handoff after success | `Open my findings` |
| `nextInspectAction` | Optional inspect fallback | existing tenant finding detail route |
Validation rules:
- Actor must remain a tenant member for the target finding.
- Actor must have `TENANT_FINDINGS_ASSIGN`.
- The locked record must still have `assignee_user_id = null` at mutation time.
- Claim leaves `owner_user_id` unchanged.
- Claim leaves workflow status unchanged.
- Success removes the row from intake immediately because the assignee is no longer null.
- Conflict does not mutate the row and must return honest feedback so the queue can refresh.
## State And Ordering Rules
### Intake inclusion order
1. Restrict to the current workspace.
2. Restrict to visible tenant IDs.
3. Restrict to `assignee_user_id IS NULL`.
4. Restrict to `Finding::openStatuses()`.
5. Apply the fixed queue view:
- `unassigned` keeps all included rows
- `needs_triage` keeps only `new` and `reopened`
6. Apply optional tenant prefilter.
7. Sort overdue rows first, reopened rows second, new rows third, then remaining backlog.
8. Within each bucket, rows with due dates sort by `dueAt` ascending, rows without due dates sort last, and remaining ties sort by `findingId` descending.
### Urgency semantics
- Overdue rows are the highest-priority bucket.
- Reopened non-overdue rows are the next bucket.
- New rows follow reopened rows.
- Triaged and in-progress unassigned rows remain visible in `Unassigned`, but never in `Needs triage`.
### Claim semantics
- Claim is not a lifecycle status transition.
- Claim performs one responsibility transition only: `assignee_user_id` moves from `null` to the current user.
- Owner accountability remains unchanged.
- Successful claim makes the finding eligible for `My Findings` immediately because the record is now assigned.
- Stale-row conflicts must fail before save when the locked record already has an assignee.
### Empty-state semantics
- If no visible intake rows exist anywhere in scope, the page shows a calm empty state with one CTA into `My Findings`.
- If the active tenant prefilter causes the empty state while other visible tenants still contain intake rows, the empty state must explain the tenant boundary and offer `Clear tenant filter`.
- Neither branch may reveal hidden tenant names or hidden queue quantities.
## Authorization-Sensitive Output
- Tenant labels, tab badges, filter values, rows, and counts are only derived from entitled tenants.
- Queue visibility remains workspace-context dependent.
- Claim affordances remain visible only inside in-scope membership context and must still enforce `403` server-side for members missing assign capability.
- Detail navigation remains tenant-scoped and must preserve existing `404` and `403` semantics on the destination.
- The derived queue state remains useful without revealing hidden tenant names, row counts, or empty-state hints.