# Data Model: Finding Outcome Taxonomy & Verification Semantics ## Overview This feature does not add a new table or a new top-level persisted entity. It reuses the current `findings` row as the source of truth for current terminal meaning, keeps reopen rationale in audit metadata, and derives verification or reporting buckets through one findings-local semantics helper. ## Entity: Finding **Persistence**: existing `findings` table **Owner**: tenant-owned record **Primary responsibility**: current workflow status, current terminal-outcome key, timestamps, and tenant-scoped operational ownership ### Relevant persisted fields | Field | Type | Source | Notes | |------|------|--------|-------| | `id` | integer | existing | Primary key | | `workspace_id` | integer | existing | Workspace isolation boundary | | `tenant_id` | integer | existing | Tenant isolation boundary | | `status` | string | existing | Primary lifecycle status; remains one of `new`, `acknowledged`, `triaged`, `in_progress`, `reopened`, `resolved`, `closed`, `risk_accepted` | | `severity` | string | existing | Existing priority and SLA signal | | `resolved_reason` | string nullable | existing | Becomes a bounded canonical resolve key instead of free-form prose | | `closed_reason` | string nullable | existing | Becomes a bounded canonical close key and remains the stored reason for `risk_accepted` | | `resolved_at` | datetime nullable | existing | Marks the current resolved terminal timestamp | | `closed_at` | datetime nullable | existing | Marks the current closed or risk-accepted terminal timestamp | | `reopened_at` | datetime nullable | existing | Marks the current reopen timestamp | | `closed_by_user_id` | integer nullable | existing | Current actor for close and risk-accept paths | | `owner_user_id` | integer nullable | existing | Accountability owner | | `assignee_user_id` | integer nullable | existing | Active assignee | | `evidence_jsonb` | jsonb | existing | Current supporting evidence; unchanged in this slice | ### Relationships | Relationship | Type | Notes | |-------------|------|-------| | `tenant()` | belongsTo | Tenant scope owner | | `ownerUser()` | belongsTo | Accountability owner | | `assigneeUser()` | belongsTo | Active assignee | | `closedByUser()` | belongsTo | Actor for close/risk accept | | `findingException()` | hasOne | Existing risk-governance truth from Spec 154 | ### Validation rules introduced by this feature | Rule | Description | |------|-------------| | `status` | No new status is introduced; all existing transition rules stay in force | | `resolved_reason` | Required for resolve and system-clear transitions; must be a canonical key from the resolve-reason family | | `closed_reason` | Required for close and risk-accept transitions; must be a canonical key from the close-reason family or the risk-accept key | | `reopen_reason` | Required for reopen transitions; remains audit metadata and must be a canonical key from the reopen-reason family | ### Canonical reason families #### Resolve reason keys | Key | Meaning | Derived verification state | |-----|---------|----------------------------| | `remediated` | Operator declares that remediation work was completed | `pending_verification` | | `no_longer_drifting` | Trusted compare no longer reproduces prior drift | `verified_cleared` | | `permission_granted` | Trusted permission evidence no longer reproduces the finding condition | `verified_cleared` | | `permission_removed_from_registry` | Trusted permission evidence confirms the triggering permission was removed | `verified_cleared` | | `role_assignment_removed` | Trusted role evidence confirms the triggering assignment was removed | `verified_cleared` | | `ga_count_within_threshold` | Trusted role evidence confirms the triggering count is now safe | `verified_cleared` | #### Close reason keys | Key | Meaning | Outcome family | |-----|---------|----------------| | `false_positive` | The finding should not have been actionable | `administrative_closure` | | `duplicate` | The finding duplicates another case | `administrative_closure` | | `no_longer_applicable` | The finding no longer applies to the tenant context | `administrative_closure` | | `accepted_risk` | Existing accepted-risk path only; still governed by exception validity | `accepted_risk` | #### Reopen reason keys | Key | Meaning | Persistence | |-----|---------|-------------| | `recurred_after_resolution` | A previously addressed condition reappeared | audit metadata only | | `verification_failed` | Trusted evidence contradicted the earlier resolved outcome | audit metadata only | | `manual_reassessment` | An operator reopened the finding after review | audit metadata only | ### Derived facets from the current row | Derived facet | Source | Meaning | |--------------|--------|---------| | `verification_state` | `status` + `resolved_reason` | `pending_verification`, `verified_cleared`, or `not_applicable` | | `terminal_outcome_key` | `status` + canonical reason key | Stable UI/reporting key such as `resolved_pending_verification`, `verified_cleared`, `closed_false_positive`, `closed_duplicate`, `closed_no_longer_applicable`, or `risk_accepted` | | `report_bucket` | `terminal_outcome_key` + governance validity | Report-friendly aggregation bucket | | `outcome_label` | `terminal_outcome_key` | Canonical operator wording | ### State transitions | From | Action | Stored result | Derived outcome | |------|--------|---------------|-----------------| | `new`, `triaged`, `in_progress`, `reopened`, `acknowledged` | `resolve(remediated)` | `status=resolved`, `resolved_reason=remediated` | `Resolved pending verification` | | open statuses | `close(false_positive)` | `status=closed`, `closed_reason=false_positive` | `Closed as false positive` | | open statuses | `close(duplicate)` | `status=closed`, `closed_reason=duplicate` | `Closed as duplicate` | | open statuses | `close(no_longer_applicable)` | `status=closed`, `closed_reason=no_longer_applicable` | `Closed as no longer applicable` | | open statuses | `riskAccept(accepted_risk)` | `status=risk_accepted`, `closed_reason=accepted_risk` | `Risk accepted` with separate governance validity | | `resolved` with `resolved_reason=remediated` | trusted system clear | `status=resolved`, `resolved_reason=` | `Verified cleared` | | open statuses | direct trusted system clear | `status=resolved`, `resolved_reason=` | `Verified cleared` | | `resolved`, `closed`, `risk_accepted` | `reopen()` | `status=reopened`, terminal reason fields cleared, reopen reason recorded in audit metadata | open finding again | ## Entity: FindingException **Persistence**: existing `finding_exceptions` table **Owner**: tenant-owned record **Primary responsibility**: governance validity for `risk_accepted` ### Relevant fields consumed by this feature | Field | Type | Notes | |------|------|-------| | `status` | string | Existing workflow for exception requests and approvals | | `current_validity_state` | string | Current validity used by `FindingRiskGovernanceResolver` | | `effective_from` | datetime nullable | Existing validity window | | `expires_at` | datetime nullable | Existing validity window | | `review_due_at` | datetime nullable | Existing governance follow-up signal | ### Rule in this feature - `FindingException` continues to determine whether `risk_accepted` is governed safely. - `FindingException` does not contribute to `verified_cleared` and does not change remediation buckets. ## Derived read model: FindingOutcomeSemantics **Persistence**: not persisted **Owner**: findings-local support helper **Primary responsibility**: unify list, detail, filter, and reporting semantics from current finding truth ### Inputs | Input | Source | |------|--------| | `status` | `Finding` row | | `resolved_reason` | `Finding` row | | `closed_reason` | `Finding` row | | `findingException` validity | `FindingException` relationship via existing resolver | | `system_origin` and prior workflow steps | audit metadata, only when reconstructing history or detailed provenance | ### Outputs | Output | Type | Notes | |-------|------|-------| | `terminalOutcomeKey` | string | Stable internal key | | `label` | string | Canonical operator-facing wording | | `verificationState` | string | `pending_verification`, `verified_cleared`, `not_applicable` | | `reportBucket` | string | Aggregation bucket for reviews and exports | | `historicalContext` | string nullable | Reuses current resolver-style explanatory text where appropriate | ### Report bucket mapping | Terminal outcome key | Report bucket | |----------------------|---------------| | `resolved_pending_verification` | `remediation_pending_verification` | | `verified_cleared` | `remediation_verified` | | `closed_false_positive` | `administrative_closure` | | `closed_duplicate` | `administrative_closure` | | `closed_no_longer_applicable` | `administrative_closure` | | `risk_accepted` | `accepted_risk` | ## Audit metadata additions or constraints | Key | Existing/New | Purpose | |-----|--------------|---------| | `resolved_reason` | existing | Canonical current resolve key | | `closed_reason` | existing | Canonical current close or risk-accept key | | `reopened_reason` | existing | Canonical reopen key for reviewability | | `system_origin` | existing | Provenance flag for system transitions | | `resolved_at`, `closed_at`, `reopened_at` | existing | Timeline reconstruction | ### Audit rule - Audit metadata remains the place to reconstruct path history, such as whether a verified-clear outcome happened directly through automation or after a prior manual `remediated` transition. - The current row remains the source of truth for current filters and summaries.