TenantAtlas/specs/231-finding-outcome-taxonomy/data-model.md
ahmido 421261a517
Some checks failed
Main Confidence / confidence (push) Failing after 48s
feat: implement finding outcome taxonomy (#267)
## Summary
- implement the finding outcome taxonomy end-to-end with canonical resolve, close, reopen, and verification semantics
- align finding UI, filters, audit metadata, review summaries, and export/read-model consumers to the shared outcome semantics
- add focused Pest coverage and complete the spec artifacts for feature 231

## Details
- manual resolve is limited to the canonical `remediated` outcome
- close and reopen flows now use bounded canonical reasons
- trusted system clear and reopen distinguish verified-clear from verification-failed and recurrence paths
- duplicate lifecycle backfill now closes findings canonically as `duplicate`
- accepted-risk recording now uses the canonical `accepted_risk` reason
- finding detail and list surfaces now expose terminal outcome and verification summaries
- review, snapshot, and review-pack consumers now propagate the same outcome buckets

## Filament / Platform Contract
- Livewire v4.0+ compatibility remains intact
- provider registration is unchanged and remains in `bootstrap/providers.php`
- no new globally searchable resource was introduced; `FindingResource` still has a View page and `TenantReviewResource` remains globally searchable false
- lifecycle mutations still run through confirmed Filament actions with capability enforcement
- no new asset family was added; the existing `filament:assets` deploy step is unchanged

## Verification
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingWorkflowServiceTest.php tests/Feature/Findings/FindingRecurrenceTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php tests/Feature/Findings/FindingRiskGovernanceProjectionTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Models/FindingResolvedTest.php tests/Unit/Findings/FindingWorkflowServiceTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewRegisterTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php`
- browser smoke: `/admin/findings/my-work` -> finding detail resolve flow -> queue regression check passed

## Notes
- this commit also includes the existing `.github/agents/copilot-instructions.md` workspace change that was already present in the worktree when all changes were committed

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #267
2026-04-23 07:29:05 +00:00

174 lines
9.6 KiB
Markdown

# 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=<system clear key>` | `Verified cleared` |
| open statuses | direct trusted system clear | `status=resolved`, `resolved_reason=<system clear key>` | `Verified cleared` |
| `resolved`, `closed`, `risk_accepted` | `reopen(<canonical reopen key>)` | `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.