# Data Model: Finding Ownership Semantics Clarification **Date**: 2026-04-20 **Branch**: `219-finding-ownership-semantics` ## Overview This feature introduces no new persisted entities. It clarifies responsibility semantics over existing finding and finding-exception records and adds one derived responsibility-state projection for operator-facing surfaces. ## Entity: Finding **Represents**: A tenant-owned operational governance finding that moves through the findings workflow and may carry both accountable ownership and active remediation assignment. ### Key Fields | Field | Type | Required | Notes | |---|---|---|---| | `id` | bigint | yes | Primary key | | `workspace_id` | bigint | yes | Derived tenant ownership boundary | | `tenant_id` | bigint | yes | Tenant isolation boundary | | `status` | string | yes | Existing findings lifecycle state | | `severity` | string | yes | Existing severity dimension | | `owner_user_id` | bigint nullable | no | Accountable person for the finding outcome | | `assignee_user_id` | bigint nullable | no | Active remediation executor / coordinator | | `due_at` | datetime nullable | no | Existing SLA/follow-up deadline | | `resolved_reason` | string nullable | no | Existing closure context | | `closed_reason` | string nullable | no | Existing closure/governance context | ### Relationships | Relationship | Target | Cardinality | Purpose | |---|---|---|---| | `tenant()` | `Tenant` | belongsTo | Tenant ownership and authorization | | `ownerUser()` | `User` | belongsTo | Accountable owner | | `assigneeUser()` | `User` | belongsTo | Active remediation assignee | | `findingException()` | `FindingException` | hasOne | Optional exception artifact for accepted-risk governance | ### Validation Rules - `owner_user_id` MAY be null. - `assignee_user_id` MAY be null. - If present, either user ID MUST reference a current member of the active tenant. - Responsibility changes are allowed only on open findings, matching the current `FindingWorkflowService::assign()` rule. ## Entity: FindingException **Represents**: A tenant-owned exception artifact attached to a finding when governance coverage is requested or granted. ### Key Fields | Field | Type | Required | Notes | |---|---|---|---| | `id` | bigint | yes | Primary key | | `finding_id` | bigint | yes | Owning finding | | `tenant_id` | bigint | yes | Tenant isolation boundary | | `owner_user_id` | bigint nullable | no | Accountable owner of the exception artifact, not of the finding itself | | `status` | string | yes | Existing exception lifecycle state | | `current_validity_state` | string nullable | no | Existing governance-validity dimension | | `request_reason` | text | yes | Existing request context | ### Relationships | Relationship | Target | Cardinality | Purpose | |---|---|---|---| | `finding()` | `Finding` | belongsTo | Parent finding context | | `owner()` | `User` | belongsTo | Exception artifact owner | ### Validation Rules - Exception-owner selection continues to use current tenant-member validation. - Exception ownership MUST remain semantically distinct from finding ownership on all mixed-context surfaces. ## Derived Projection: ResponsibilityState **Represents**: An operator-facing derived state computed from `owner_user_id` and `assignee_user_id` without new persistence. **Naming convention**: - Operator-facing UI label: `orphaned accountability` - Internal derived-state and contract slug: `orphaned_accountability` ### Derived Values | Derived State | Rule | Operator Meaning | |---|---|---| | `orphaned_accountability` | `owner_user_id == null` | No accountable owner is set. This remains true even if an assignee exists. | | `owned_unassigned` | `owner_user_id != null && assignee_user_id == null` | Someone owns the outcome, but active remediation work is not assigned. | | `assigned` | `owner_user_id != null && assignee_user_id != null` | Accountability and active remediation assignment are both set. | ### Rendering Notes - If owner and assignee are the same user, the state remains `assigned`; the UI should show both roles satisfied without implying a data problem. - If both are null, the finding still uses the slug `orphaned_accountability` and the visible label `orphaned accountability`. - If assignee is present but owner is null, the finding remains `orphaned_accountability`; the UI may also show that remediation is assigned without accountable ownership. ## Mutation Contract: ResponsibilityUpdate **Represents**: The input/output contract of the existing assignment action. ### Input Shape | Field | Type | Required | Notes | |---|---|---|---| | `owner_user_id` | bigint nullable | no | Set, change, or clear finding owner | | `assignee_user_id` | bigint nullable | no | Set, change, or clear finding assignee | ### Behavioral Rules - The existing `FindingWorkflowService::assign()` method remains the mutation boundary. - The service MUST continue to write both fields explicitly to the finding. - Operator feedback and audit-facing wording should classify the result as `owner_only`, `assignee_only`, `clear_owner`, `clear_assignee`, or `owner_and_assignee` when both fields change in one update. ## State and Lifecycle Impact This feature does not add a new lifecycle family. It overlays responsibility semantics on top of existing findings lifecycle states. | Existing Lifecycle State | Responsibility Impact | |---|---| | `new`, `triaged`, `in_progress`, `reopened`, `acknowledged` | Responsibility state is actionable and visible by default | | `resolved`, `closed` | Responsibility remains historical context only | | `risk_accepted` | Responsibility remains visible, but exception-owner context may also appear and must remain separate |