# Data Model: Stored Reports Surface v1 **Date**: 2026-05-06 **Branch**: `277-stored-reports-surface` ## Overview This slice introduces no new persisted entity. `StoredReport` remains the only stored-report source of truth. The new surface adds tenant-scoped, read-only view models over that truth and keeps downstream evidence, review, and review-pack consumers separate. ## Existing Persisted Truth ### 1. Stored Report **Persistence**: Existing `stored_reports` table **Ownership**: Tenant-owned **Scope**: Many retained rows per tenant and report family | Field | Type | Nullable | Notes | |-------|------|----------|-------| | `id` | bigint | no | Internal stored-report id | | `workspace_id` | bigint | no | Required workspace isolation anchor | | `tenant_id` | bigint | no | Required tenant isolation anchor | | `report_type` | string | no | Current v1 supported values: `permission_posture`, `entra.admin_roles` | | `payload` | jsonb | no | Family-specific report payload | | `fingerprint` | string | yes | Integrity anchor for the current row | | `previous_fingerprint` | string | yes | Historical lineage anchor | | `created_at` | datetime | yes | Persisted creation time | | `updated_at` | datetime | yes | Standard timestamp | **Behavior rules**: - Stored reports are immutable retained artifacts after creation. - `current` versus `historical` is derived by comparing the row with the latest row for the same `tenant_id` and `report_type`. - `retention_state` stays derived as `retained` in this slice. - No new persisted lifecycle, publication, or browse metadata is introduced. ### 2. Evidence Snapshot Item Source Reference **Persistence**: Existing `evidence_snapshot_items.source_record_type` and `source_record_id` fields **Ownership**: Existing evidence domain These fields remain downstream identity anchors when evidence items point to a stored report. They are context only in this slice and do not create an additional v1 launch seam. ### 3. Review Pack / Tenant Review Consumption **Persistence**: Existing review-pack and tenant-review summaries **Ownership**: Existing reporting domains These consumers remain separate business truth. The stored-report surface may describe that stored reports are reused downstream, but it does not converge their operator routes in v1. ## Derived Read Models ### 4. Stored Report Row Summary **Persistence**: none, derived at runtime **Owner**: stored-report register | Field | Type | Required | Notes | |-------|------|----------|-------| | `report_id` | int | yes | Backed by `stored_reports.id` | | `display_reference` | string | yes | `Stored report #{id} ({family label})` from existing artifact-truth wording | | `report_type` | string | yes | Raw family key | | `report_family_label` | string | yes | Headline-style label such as `Permission posture report` | | `lifecycle_state` | string | yes | `current` or `historical` from artifact truth | | `retention_state` | string | yes | `retained` | | `measured_at` | datetime | yes | Derived from payload timestamp when present, otherwise `created_at` | | `summary_highlights` | list | yes | Bounded, family-specific summary facts | | `history_visibility` | bool | yes | Whether the row is included only when history is revealed | ### 5. Stored Report Detail View **Persistence**: none, derived at runtime **Owner**: stored-report detail page | Field | Type | Required | Notes | |-------|------|----------|-------| | `artifact_truth` | array | yes | Reused `ArtifactTruthPresenter::forStoredReport()` envelope | | `display_reference` | string | yes | Stable stored-report reference | | `report_family_label` | string | yes | Headline family label | | `measured_at` | datetime | yes | Payload timestamp or `created_at` | | `integrity_anchor` | string | no | `fingerprint`, shown when present | | `previous_fingerprint` | string | no | Historical lineage anchor | | `current_report_id` | int | no | Latest same-family row when viewing a historical record | | `current_report_url` | string | no | Canonical detail URL for the current row | | `summary_branch` | object | yes | One of the two supported family-specific summary shapes below | | `raw_payload` | array | yes | Present but collapsed by default | ### 6. Permission Posture Summary **Persistence**: none, derived from `StoredReport.payload` Payload anchors from current repo truth: - `posture_score` - `required_count` - `granted_count` - `checked_at` - `permissions[]` entries with `key`, `type`, `status`, and `features` Derived summary fields: | Field | Type | Notes | |-------|------|-------| | `posture_score` | int or null | Primary posture score | | `required_count` | int | Required permissions count | | `granted_count` | int | Granted permissions count | | `missing_count` | int | Derived as `required_count - granted_count`, never persisted | | `at_risk_permissions` | list | First few non-granted permission entries for operator summary | | `checked_at` | datetime or null | Preferred measurement timestamp when present | ### 7. Entra Admin Roles Summary **Persistence**: none, derived from `StoredReport.payload` Payload anchors from current repo truth: - `measured_at` - `totals.roles_total` - `totals.assignments_total` - `totals.high_privilege_assignments` - `high_privilege[]` entries with role, principal, scope, and severity fields Derived summary fields: | Field | Type | Notes | |-------|------|-------| | `roles_total` | int | Total role definitions captured | | `assignments_total` | int | Total assignments captured | | `high_privilege_assignments` | int | Count of privileged assignments | | `highest_risk_assignment` | array or null | First highest-severity assignment for operator context | | `measured_at` | datetime or null | Preferred measurement timestamp | ### 8. Stored Report Launch Seam **Persistence**: none, derived from existing operator routes | Field | Type | Required | Notes | |-------|------|----------|-------| | `source_surface` | string | yes | Current v1 source surface name | | `target_url` | string | yes | Canonical stored-report detail URL | **Current v1 seam**: - `AdminRolesSummaryWidget` is the only confirmed canonical drilldown source in this slice. ## Derived State Rules | Rule | Derived Behavior | |------|------------------| | Current row selection | Latest row by `created_at desc, id desc` for the same `tenant_id` and `report_type` | | Lifecycle state | `current` for the latest row, otherwise `historical` | | Retention state | `retained` for v1 stored-report browsing | | Measured time | `payload.measured_at` or `payload.checked_at` when available, otherwise `created_at` | | Register visibility | Only rows with an explicit family-to-capability mapping are listed | | Detail authorization | Family-aware capability check after workspace and tenant membership is established | | Unexpected report family | Outside v1 browse and detail scope until a follow-up spec adds support | ## Boundaries Explicitly Preserved - No new report-generation, export, or lifecycle-mutation truth is introduced. - Evidence snapshots, tenant reviews, review packs, and stored reports remain separate artifacts. - Current versus historical stays derived and is not persisted as a new domain state. - No generic report registry, schema catalog, or renderer framework is created. - Evidence, review, and review-pack routes remain context only and are not v1 convergence targets.