TenantAtlas/specs/162-baseline-gap-details/data-model.md
ahmido 7d4d607475 feat: add baseline gap details surfaces (#192)
## Summary
- add baseline compare evidence gap detail modeling and a dedicated Livewire table surface
- extend baseline compare landing and operation run detail surfaces to expose evidence gap details and stats
- add spec artifacts for feature 162 and expand feature coverage with focused Filament and baseline tests

## Notes
- branch: `162-baseline-gap-details`
- commit: `a92dd812`
- working tree was clean after push

## Validation
- tests were not run in this step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #192
2026-03-24 19:05:23 +00:00

140 lines
5.0 KiB
Markdown

# 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"
]
}
}
}
}
```