# Data Model: Enterprise Evidence Gap Details for Baseline Compare

## Overview

This feature does not introduce new database tables. It extends the tenant-owned `OperationRun` JSONB context for `baseline_compare` runs and defines a derived operator-facing row model for evidence-gap detail rendering.

## Entity: OperationRun

- Ownership: Tenant-owned operational artifact with `workspace_id` and `tenant_id`
- Storage: Existing `operation_runs` table
- Relevant relationships:
  - Belongs to `Tenant`
  - Belongs to `Workspace`
  - Belongs to initiating `User`
- Relevant invariant:
  - `status` and `outcome` remain service-owned through `OperationRunService`
  - `context` may be enriched by the compare job without changing lifecycle semantics

## Subdocument: `baseline_compare`

Stored under `OperationRun.context['baseline_compare']`.

### Fields

| Field | Type | Required | Description |
|---|---|---|---|
| `inventory_sync_run_id` | integer | no | Source inventory sync run used for coverage and freshness context |
| `subjects_total` | integer | no | Total compare subjects considered for the run |
| `evidence_capture` | object | no | Capture stats such as requested, succeeded, skipped, failed, throttled |
| `coverage` | object | no | Coverage proof and covered/uncovered policy types |
| `fidelity` | string | no | Compare fidelity, typically `meta` or `content` |
| `reason_code` | string | no | Top-level compare reason code used by the explanation layer |
| `resume_token` | string or null | no | Resume state for incomplete content capture |
| `evidence_gaps` | object | no | Aggregate and subject-level evidence-gap contract |

## Subdocument: `baseline_compare.evidence_gaps`

### Fields

| Field | Type | Required | Description |
|---|---|---|---|
| `count` | integer | no | Total evidence-gap count across all reasons |
| `by_reason` | object<string,int> | no | Aggregate counts keyed by evidence-gap reason code |
| `subjects` | object<string,list<string>> | no | Bounded reason-grouped list of concrete subject keys |

### Validation Rules

- `count` must be a non-negative integer.
- Each `by_reason` value must be a non-negative integer.
- Each `subjects` key must be a non-empty reason code string.
- Each `subjects[reason]` list item must be a non-empty string in `policy_type|subject_key` format.
- Subject lists are deduplicated per reason.
- Subject lists are capped at the compare job limit, currently 50 items per reason.

## Derived Entity: EvidenceGapDetailRow

This is not stored separately. It is derived from `baseline_compare.evidence_gaps.subjects` for rendering and filtering.

### Fields

| Field | Type | Source | Description |
|---|---|---|---|
| `reason_code` | string | map key | Stable reason identifier such as `ambiguous_match` or `policy_not_found` |
| `reason_label` | string | derived | Operator-facing label from `reason_code` |
| `policy_type` | string | parsed from subject string | Policy family segment before the first pipe |
| `subject_key` | string | parsed from subject string | Subject identity segment after the first pipe |
| `search_text` | string | derived | Lowercased concatenation of reason, policy type, and subject key for local filtering |

### Validation Rules

- `policy_type` must be non-empty.
- `subject_key` may be human-readable text, GUID-like values, or workspace-safe identifiers, but must be non-empty once persisted.
- `search_text` must be deterministic and derived only from persisted row values.

## Derived Entity: EvidenceGapReasonBucket

Groups detail rows for rendering.

### Fields

| Field | Type | Description |
|---|---|---|
| `reason_code` | string | Stable bucket key |
| `reason_label` | string | Operator-facing label |
| `count` | integer | Number of visible persisted subjects in the bucket |
| `rows` | list<EvidenceGapDetailRow> | Rows shown for the reason |

## State Model

### Run detail evidence-gap states

1. `NoGaps`
   - `evidence_gaps.count` is absent or `0`
   - No evidence-gap detail section is required

2. `GapsWithRecordedSubjects`
   - `evidence_gaps.count > 0`
   - `evidence_gaps.subjects` exists with at least one non-empty reason bucket
   - Render searchable grouped rows

3. `GapsWithoutRecordedSubjects`
   - `evidence_gaps.count > 0`
   - `evidence_gaps.subjects` is absent or empty
   - Render explicit fallback copy indicating detail was not recorded for that run

## Example Payload

```json
{
  "baseline_compare": {
    "subjects_total": 50,
    "fidelity": "meta",
    "reason_code": "evidence_capture_incomplete",
    "evidence_capture": {
      "requested": 50,
      "succeeded": 47,
      "skipped": 0,
      "failed": 3,
      "throttled": 0
    },
    "evidence_gaps": {
      "count": 5,
      "by_reason": {
        "ambiguous_match": 3,
        "policy_not_found": 2
      },
      "subjects": {
        "ambiguous_match": [
          "deviceConfiguration|WiFi-Corp-Profile",
          "deviceConfiguration|VPN-Always-On"
        ],
        "policy_not_found": [
          "deviceConfiguration|Deleted-Policy-ABC"
        ]
      }
    }
  }
}
```
