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