204 lines
8.6 KiB
Markdown
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. |