--- phase: 15-consolidation-data-model plan: 01 type: execute wave: 1 depends_on: [] files_modified: - SharepointToolbox/Core/Models/LocationInfo.cs - SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs - SharepointToolbox/Core/Helpers/PermissionConsolidator.cs autonomous: true requirements: - RPT-04 must_haves: truths: - "LocationInfo record holds five location fields from UserAccessEntry" - "ConsolidatedPermissionEntry holds key fields plus a list of LocationInfo with LocationCount" - "PermissionConsolidator.Consolidate merges entries with identical key into single rows" - "MakeKey uses pipe-delimited case-insensitive composite of UserLogin+PermissionLevel+AccessType+GrantedThrough" - "Empty input returns empty list" artifacts: - path: "SharepointToolbox/Core/Models/LocationInfo.cs" provides: "Location data record" contains: "public record LocationInfo" - path: "SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs" provides: "Consolidated permission model" contains: "public record ConsolidatedPermissionEntry" - path: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs" provides: "Consolidation logic" exports: ["Consolidate", "MakeKey"] key_links: - from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs" to: "SharepointToolbox/Core/Models/UserAccessEntry.cs" via: "accepts IReadOnlyList" pattern: "IReadOnlyList" - from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs" to: "SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs" via: "returns IReadOnlyList" pattern: "IReadOnlyList" - from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs" to: "SharepointToolbox/Core/Models/LocationInfo.cs" via: "constructs LocationInfo from UserAccessEntry fields" pattern: "new LocationInfo" --- Create the consolidation data model and merge service for permission report consolidation. Purpose: Establish the data shape (LocationInfo, ConsolidatedPermissionEntry) and pure-function merge logic (PermissionConsolidator) so that Phase 16 can wire them into the export pipeline. Zero API calls, zero UI — just models and a static helper. Output: Three production files — two model records and one static consolidation service. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/15-consolidation-data-model/15-CONTEXT.md @.planning/phases/15-consolidation-data-model/15-RESEARCH.md ```csharp namespace SharepointToolbox.Core.Models; public enum AccessType { Direct, Group, Inherited } public record UserAccessEntry( string UserDisplayName, string UserLogin, string SiteUrl, string SiteTitle, string ObjectType, string ObjectTitle, string ObjectUrl, string PermissionLevel, AccessType AccessType, string GrantedThrough, bool IsHighPrivilege, bool IsExternalUser ); ``` Task 1: Create LocationInfo and ConsolidatedPermissionEntry model records SharepointToolbox/Core/Models/LocationInfo.cs, SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs Create two new C# positional record files in `Core/Models/`. **LocationInfo.cs** — namespace `SharepointToolbox.Core.Models`: ```csharp public record LocationInfo( string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType ); ``` Lightweight record holding the five location-related fields extracted from UserAccessEntry when rows are merged. **ConsolidatedPermissionEntry.cs** — namespace `SharepointToolbox.Core.Models`: ```csharp public record ConsolidatedPermissionEntry( string UserDisplayName, string UserLogin, string PermissionLevel, AccessType AccessType, string GrantedThrough, bool IsHighPrivilege, bool IsExternalUser, IReadOnlyList Locations ) { public int LocationCount => Locations.Count; } ``` - Holds the four key fields (UserLogin, PermissionLevel, AccessType, GrantedThrough) plus carried-forward fields (UserDisplayName, IsHighPrivilege, IsExternalUser). - `Locations` is an `IReadOnlyList` containing all merged locations. - `LocationCount` is a computed convenience property. - Do NOT add any methods, constructors, or logic beyond the record definition and LocationCount property. cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 Both record files exist, compile without errors, and are in the SharepointToolbox.Core.Models namespace. ConsolidatedPermissionEntry.LocationCount returns Locations.Count. Task 2: Create PermissionConsolidator static helper SharepointToolbox/Core/Helpers/PermissionConsolidator.cs Create `PermissionConsolidator.cs` in `Core/Helpers/` — namespace `SharepointToolbox.Core.Helpers`. Follow the existing `DuplicatesService.MakeKey()` pattern for composite key generation. ```csharp using SharepointToolbox.Core.Models; namespace SharepointToolbox.Core.Helpers; /// /// Merges a flat list of UserAccessEntry rows into consolidated entries /// where rows with identical (UserLogin, PermissionLevel, AccessType, GrantedThrough) /// are grouped into a single row with multiple locations. /// public static class PermissionConsolidator { /// /// Builds a pipe-delimited, case-insensitive composite key from the four key fields. /// internal static string MakeKey(UserAccessEntry entry) { return string.Join("|", entry.UserLogin.ToLowerInvariant(), entry.PermissionLevel.ToLowerInvariant(), entry.AccessType.ToString(), entry.GrantedThrough.ToLowerInvariant()); } /// /// Groups entries by composite key and returns consolidated rows. /// Each group's first entry provides UserDisplayName, IsHighPrivilege, IsExternalUser. /// All entries in a group contribute a LocationInfo to the Locations list. /// Results are ordered by UserLogin then PermissionLevel. /// public static IReadOnlyList Consolidate( IReadOnlyList entries) { if (entries.Count == 0) return Array.Empty(); return entries .GroupBy(e => MakeKey(e)) .Select(g => { var first = g.First(); var locations = g.Select(e => new LocationInfo( e.SiteUrl, e.SiteTitle, e.ObjectTitle, e.ObjectUrl, e.ObjectType )).ToList(); return new ConsolidatedPermissionEntry( first.UserDisplayName, first.UserLogin, first.PermissionLevel, first.AccessType, first.GrantedThrough, first.IsHighPrivilege, first.IsExternalUser, locations); }) .OrderBy(c => c.UserLogin) .ThenBy(c => c.PermissionLevel) .ToList(); } } ``` Key implementation details: - `MakeKey` is `internal` so tests can access it via `[InternalsVisibleTo]` or by testing through `Consolidate`. - Use `.ToLowerInvariant()` on UserLogin, PermissionLevel, GrantedThrough (string key fields). AccessType is an enum — use `.ToString()` (case-stable). - Empty input short-circuits to `Array.Empty<>()`. - LINQ GroupBy + Select pattern — no mutable dictionaries. - OrderBy UserLogin then PermissionLevel for deterministic output. cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 PermissionConsolidator.cs compiles. Consolidate method accepts IReadOnlyList of UserAccessEntry, returns IReadOnlyList of ConsolidatedPermissionEntry. MakeKey produces pipe-delimited lowercase composite key. Full solution build passes: ```bash cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build --no-restore -v q ``` - LocationInfo.cs and ConsolidatedPermissionEntry.cs exist in Core/Models/ with correct record signatures - PermissionConsolidator.cs exists in Core/Helpers/ with Consolidate and MakeKey methods - All three files compile as part of the SharepointToolbox project - No changes to any existing files After completion, create `.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md`