---
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<UserAccessEntry>"
      pattern: "IReadOnlyList<UserAccessEntry>"
    - from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
      to: "SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs"
      via: "returns IReadOnlyList<ConsolidatedPermissionEntry>"
      pattern: "IReadOnlyList<ConsolidatedPermissionEntry>"
    - from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
      to: "SharepointToolbox/Core/Models/LocationInfo.cs"
      via: "constructs LocationInfo from UserAccessEntry fields"
      pattern: "new LocationInfo"
---

<objective>
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.
</objective>

<execution_context>
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- Source model the consolidator consumes. From SharepointToolbox/Core/Models/UserAccessEntry.cs -->
```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
);
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create LocationInfo and ConsolidatedPermissionEntry model records</name>
  <files>SharepointToolbox/Core/Models/LocationInfo.cs, SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs</files>
  <action>
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<LocationInfo> 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<LocationInfo>` 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.
  </action>
  <verify>
    <automated>cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
  </verify>
  <done>Both record files exist, compile without errors, and are in the SharepointToolbox.Core.Models namespace. ConsolidatedPermissionEntry.LocationCount returns Locations.Count.</done>
</task>

<task type="auto">
  <name>Task 2: Create PermissionConsolidator static helper</name>
  <files>SharepointToolbox/Core/Helpers/PermissionConsolidator.cs</files>
  <action>
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;

/// <summary>
/// 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.
/// </summary>
public static class PermissionConsolidator
{
    /// <summary>
    /// Builds a pipe-delimited, case-insensitive composite key from the four key fields.
    /// </summary>
    internal static string MakeKey(UserAccessEntry entry)
    {
        return string.Join("|",
            entry.UserLogin.ToLowerInvariant(),
            entry.PermissionLevel.ToLowerInvariant(),
            entry.AccessType.ToString(),
            entry.GrantedThrough.ToLowerInvariant());
    }

    /// <summary>
    /// 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.
    /// </summary>
    public static IReadOnlyList<ConsolidatedPermissionEntry> Consolidate(
        IReadOnlyList<UserAccessEntry> entries)
    {
        if (entries.Count == 0)
            return Array.Empty<ConsolidatedPermissionEntry>();

        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.
  </action>
  <verify>
    <automated>cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
  </verify>
  <done>PermissionConsolidator.cs compiles. Consolidate method accepts IReadOnlyList of UserAccessEntry, returns IReadOnlyList of ConsolidatedPermissionEntry. MakeKey produces pipe-delimited lowercase composite key.</done>
</task>

</tasks>

<verification>
Full solution build passes:
```bash
cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build --no-restore -v q
```
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md`
</output>
