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