---
phase: 15
title: Consolidation Data Model
status: ready-for-planning
created: 2026-04-09
---

# Phase 15 Context: Consolidation Data Model

## Decided Areas (from prior research + STATE.md)

These are locked — do not re-litigate during planning or execution.

| Decision | Value |
|---|---|
| Consolidation scope | User access audit report only — site-centric permission report is unchanged |
| Source model | `UserAccessEntry` (already normalized, one user per row) |
| Consolidation is opt-in | Defaults to OFF; toggle wired in Phase 16 |
| No API calls | Pure data transformation — no Graph or CSOM calls |
| Existing exports unchanged | When consolidation is not applied, output is identical to pre-v2.3 |

## Discussed Areas

### 1. Consolidation Key (What Defines "Same Access")

**Decision:** Merge rows only when all four fields match: `UserLogin` + `PermissionLevel` + `AccessType` + `GrantedThrough`.

- Strictest matching — preserves the audit trail of how access was granted
- A user with "Contribute (Direct)" on 3 sites and "Contribute (Group: Members)" on 2 sites produces 2 consolidated rows, not 1
- `UserLogin` is the identity key (not `UserDisplayName`, which could vary)
- `AccessType` enum values: Direct, Group, Inherited — all treated as distinct
- `GrantedThrough` string comparison is exact (e.g., "SharePoint Group: Members" vs "SharePoint Group: Owners" are separate)

### 2. Merged Locations Model

**Decision:** `List<LocationInfo>` with a `LocationCount` convenience property.

- `ConsolidatedPermissionEntry` holds all fields from the consolidation key plus a `List<LocationInfo>` containing each merged site's URL and title
- `LocationInfo` is a lightweight record: `{ string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType }`
- `LocationCount` is a computed property (`Locations.Count`) — convenience for display and sorting
- No information loss — all original location data is preserved in the list
- Presentation decisions (how to render the list) are deferred to Phase 16

### 3. Report Scope

**Decision:** Consolidation applies to user access audit (`UserAccessEntry`) only.

- The user access audit report is already user-centric and normalized (one user per row) — natural fit for "merge same user across locations"
- The site-centric permission report (`PermissionEntry`) flows the opposite direction (site → users); consolidating it would mean "same permission set across sites" — a different feature entirely
- `HtmlExportService` (site-centric) is untouched by this phase
- `UserAccessHtmlExportService` will receive consolidated data in Phase 16; this phase only builds the model and service

## Deferred Ideas (out of scope for Phase 15)

- Consolidation toggle UI (Phase 16)
- Consolidated view rendering in HTML exports (Phase 16)
- Group expansion within consolidated rows (Phase 17)
- Consolidation in CSV exports (out of scope per REQUIREMENTS.md)
- "Same permission set across sites" consolidation for site-centric report (not planned)

## code_context

| Asset | Path | Reuse |
|---|---|---|
| UserAccessEntry model | `SharepointToolbox/Core/Models/UserAccessEntry.cs` | Source model — consolidated entry mirrors its fields + locations list |
| UserAccessAuditService | `SharepointToolbox/Services/UserAccessAuditService.cs` | Produces the `UserAccessEntry` list that feeds the consolidator |
| UserAccessHtmlExportService | `SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs` | Downstream consumer in Phase 16 — must accept both flat and consolidated lists |
| DuplicatesService grouping pattern | `SharepointToolbox/Services/DuplicatesService.cs` | Reference for composite-key grouping via `MakeKey()` pattern |
| PermissionSummaryBuilder | `SharepointToolbox/Core/Helpers/PermissionSummaryBuilder.cs` | Reference for aggregation pattern over permission data |
| Test project | `SharepointToolbox.Tests/` | New tests for PermissionConsolidator with known input/output pairs |
