TenantAtlas/specs/219-finding-ownership-semantics/data-model.md
Ahmed Darrazi 1741b22203 docs: amend constitution to v2.7.0 (LEAN-001 pre-production lean doctrine)
- Add LEAN-001 to constitution after BIAS-001: forbids legacy aliases,
  migration shims, dual-write logic, and compatibility fixtures in a
  pre-production codebase
- Add compatibility posture default block to spec template
- Add pre-production compatibility check to agent instructions
- Unify backup_set operation type to canonical backup_set.update
- Remove all legacy backup_set.add_policies/remove_policies references
- Add finding ownership semantics (responsibility/accountability labels)
- Clean up roadmap.md and spec-candidates.md
2026-04-20 19:53:04 +02:00

121 lines
5.6 KiB
Markdown

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