Two plans for Phase 15: models + consolidator service (wave 1), unit tests + build verification (wave 2). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
240 lines
9.1 KiB
Markdown
240 lines
9.1 KiB
Markdown
---
|
|
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>
|