381 lines
24 KiB
Markdown
381 lines
24 KiB
Markdown
# Phase 16: Report Consolidation Toggle - Research
|
|
|
|
**Researched:** 2026-04-09
|
|
**Domain:** WPF MVVM toggle / HTML export rendering / CSV export extension
|
|
**Confidence:** HIGH
|
|
|
|
## Summary
|
|
|
|
Phase 16 wires the `PermissionConsolidator.Consolidate()` helper from Phase 15 into the actual export flow via a user-visible checkbox. All foundational models (`ConsolidatedPermissionEntry`, `LocationInfo`, `UserAccessEntry`) and the consolidation algorithm are complete and tested. This phase is purely integration work: add a `MergePermissions` bool property to the ViewModel, bind a new "Export Options" GroupBox in both XAML views, and branch inside `BuildHtml` / `BuildCsv` / `WriteSingleFileAsync` based on that flag.
|
|
|
|
The consolidation path for HTML requires rendering a different by-user table structure — one row per `ConsolidatedPermissionEntry` with a "Sites" column that shows either an inline site title (1 location) or an `[N sites]` badge that expands an inline sub-list via the existing `toggleGroup()` JS. The by-site view must be hidden when consolidation is ON. The CSV path replaces flat per-user rows with a "Locations" column that lists all site titles separated by semicolons (or similar). The by-site view is disabled by hiding/graying the view-toggle buttons in the HTML; the ViewModel `IsGroupByUser` WPF DataGrid grouping is unrelated (it controls the WPF DataGrid, not the HTML export).
|
|
|
|
The session-global toggle lives as a single `[ObservableProperty] bool _mergePermissions` on `UserAccessAuditViewModel`. The `PermissionsViewModel` receives the same bool property as a no-op placeholder. Because both ViewModels are independent instances (no shared service is required), the CONTEXT.md decision of "session-scoped global setting" is implemented by putting the property in the ViewModel and never persisting it — it resets to `false` on app restart by default initialization.
|
|
|
|
**Primary recommendation:** Add `MergePermissions` to `UserAccessAuditViewModel`, bind it in both XAML tabs, pass it to `BuildHtml`/`BuildCsv`, and add a consolidated rendering branch inside those services. Keep the non-consolidated path byte-identical to the current output (no structural changes to the existing code path).
|
|
|
|
---
|
|
|
|
<user_constraints>
|
|
## User Constraints (from CONTEXT.md)
|
|
|
|
### Locked Decisions
|
|
| Decision | Value |
|
|
|---|---|
|
|
| Consolidation scope | User access audit report only — site-centric permission report is unchanged |
|
|
| Consolidation key | `UserLogin + PermissionLevel + AccessType + GrantedThrough` (4-field match) |
|
|
| Source model | `UserAccessEntry` (already normalized, one user per row) |
|
|
| Consolidation defaults OFF | Toggle must default to unchecked |
|
|
| No API calls | Pure data transformation via `PermissionConsolidator.Consolidate()` |
|
|
| Existing exports unchanged when OFF | Output must be identical to pre-v2.3 when toggle is OFF |
|
|
|
|
### Discussed (Locked)
|
|
- **Toggle Placement**: New "Export Options" GroupBox in the left panel of both audit tabs, always visible; single `CheckBox` labeled "Merge duplicate permissions"; follows the same pattern as the existing "Scan Options" GroupBox (lines 199-210 of `UserAccessAuditView.xaml`)
|
|
- **Toggle applies to both HTML and CSV exports** (user override of REQUIREMENTS.md CSV exclusion)
|
|
- **Consolidated HTML rendering**: by-user view consolidated rows with "Sites" column; 1 location = inline title; 2+ locations = `[N sites]` badge expanding inline sub-list via existing `toggleGroup()` JS pattern; by-site view is disabled/hidden when consolidation is ON
|
|
- **Session persistence**: session-scoped property on ViewModel — resets to OFF on app restart; global state shared across all tabs (both ViewModels read/write same logical property)
|
|
- **PermissionsViewModel**: toggle present in UI but no-op — stores value, does not apply consolidation
|
|
|
|
### Claude's Discretion
|
|
- Exact CSS for the `[N sites]` badge (follow existing `.badge` class style already in `UserAccessHtmlExportService.cs`)
|
|
- CSV consolidated column format (semicolon-separated site titles is the most natural approach)
|
|
- Whether `BuildHtml` takes a `bool mergePermissions` parameter or a richer options object (bool parameter is simpler given only one flag at this phase)
|
|
|
|
### Deferred Ideas (OUT OF SCOPE)
|
|
- Site-centric consolidation logic (toggle present in UI but no-op for site-centric exports)
|
|
- Group expansion within consolidated rows (Phase 17)
|
|
- Persistent consolidation preference across app restarts (session-only for now)
|
|
- "Consolidated view" report header indicator (decided: not needed)
|
|
</user_constraints>
|
|
|
|
---
|
|
|
|
<phase_requirements>
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|-----------------|
|
|
| RPT-03 | User can enable/disable entry consolidation per export (toggle in export settings) | Toggle as `[ObservableProperty] bool _mergePermissions` in `UserAccessAuditViewModel`; `PermissionsViewModel` has same property as placeholder; both XAML views get "Export Options" GroupBox; `BuildHtml` and `BuildCsv`/`WriteSingleFileAsync` accept `bool mergePermissions` and branch accordingly |
|
|
</phase_requirements>
|
|
|
|
---
|
|
|
|
## Standard Stack
|
|
|
|
### Core
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| CommunityToolkit.Mvvm | already referenced | `[ObservableProperty]` source gen for `MergePermissions` bool | Same pattern used for `IncludeInherited`, `ScanFolders`, `IncludeSubsites` throughout both ViewModels |
|
|
| WPF / XAML | .NET 10 Windows | GroupBox + CheckBox binding | Established UI pattern in this codebase |
|
|
| `PermissionConsolidator.Consolidate()` | Phase 15 (complete) | Transforms `IReadOnlyList<UserAccessEntry>` → `IReadOnlyList<ConsolidatedPermissionEntry>` | Purpose-built for this exact use case |
|
|
|
|
### No New Dependencies
|
|
Phase 16 adds zero new NuGet packages. All required types and services are already present.
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended Project Structure Changes
|
|
|
|
```
|
|
SharepointToolbox/
|
|
├── ViewModels/Tabs/
|
|
│ ├── UserAccessAuditViewModel.cs ← add MergePermissions [ObservableProperty]
|
|
│ └── PermissionsViewModel.cs ← add MergePermissions [ObservableProperty] (no-op)
|
|
├── Views/Tabs/
|
|
│ ├── UserAccessAuditView.xaml ← add Export Options GroupBox after Scan Options GroupBox
|
|
│ └── PermissionsView.xaml ← add Export Options GroupBox after Display Options GroupBox
|
|
└── Services/Export/
|
|
├── UserAccessHtmlExportService.cs ← BuildHtml(entries, mergePermissions, branding)
|
|
└── UserAccessCsvExportService.cs ← BuildCsv(…, mergePermissions) + WriteSingleFileAsync(…, mergePermissions)
|
|
|
|
SharepointToolbox.Tests/
|
|
├── Services/Export/
|
|
│ └── UserAccessHtmlExportServiceTests.cs ← add consolidated path tests
|
|
│ └── UserAccessCsvExportServiceTests.cs ← add consolidated path tests
|
|
└── ViewModels/
|
|
└── UserAccessAuditViewModelTests.cs ← add MergePermissions property test
|
|
```
|
|
|
|
### Pattern 1: ObservableProperty Bool on ViewModel (CommunityToolkit.Mvvm)
|
|
|
|
**What:** Declare a source-generated bool property that defaults to `false`.
|
|
**When to use:** Every scan/export option toggle in this codebase uses this pattern.
|
|
|
|
```csharp
|
|
// In UserAccessAuditViewModel.cs — matches existing IncludeInherited / ScanFolders pattern
|
|
[ObservableProperty]
|
|
private bool _mergePermissions;
|
|
```
|
|
|
|
The source generator emits `MergePermissions` property with `OnPropertyChanged`. No partial handler needed unless additional side effects are required (none needed here).
|
|
|
|
### Pattern 2: GroupBox + CheckBox in XAML (following Scan Options pattern)
|
|
|
|
**What:** A new GroupBox labeled "Export Options" with a single CheckBox, placed below Scan Options in the left panel DockPanel.
|
|
**Reference location:** `UserAccessAuditView.xaml` lines 199-210 — the existing "Scan Options" GroupBox.
|
|
|
|
```xml
|
|
<!-- Export Options (always visible) — add after Scan Options GroupBox -->
|
|
<GroupBox Header="Export Options"
|
|
DockPanel.Dock="Top" Margin="0,0,0,8" Padding="8">
|
|
<StackPanel>
|
|
<CheckBox Content="Merge duplicate permissions"
|
|
IsChecked="{Binding MergePermissions}" />
|
|
</StackPanel>
|
|
</GroupBox>
|
|
```
|
|
|
|
**Localization note:** The project uses `TranslationSource.Instance` for all user-visible strings. New keys are required: `audit.grp.export` and `chk.merge.permissions` in both `Strings.resx` and `Strings.fr.resx`.
|
|
|
|
### Pattern 3: Export Service Signature Extension
|
|
|
|
**What:** Add `bool mergePermissions = false` parameter to `BuildHtml`, `BuildCsv`, and `WriteSingleFileAsync`. Callers in the ViewModel pass `MergePermissions`.
|
|
**Why a plain bool:** Only one flag at this phase; using a richer options object adds indirection with no benefit yet.
|
|
|
|
```csharp
|
|
// UserAccessHtmlExportService
|
|
public string BuildHtml(
|
|
IReadOnlyList<UserAccessEntry> entries,
|
|
bool mergePermissions = false,
|
|
ReportBranding? branding = null)
|
|
{
|
|
if (mergePermissions)
|
|
{
|
|
var consolidated = PermissionConsolidator.Consolidate(entries);
|
|
return BuildConsolidatedHtml(consolidated, entries, branding);
|
|
}
|
|
// existing path unchanged
|
|
...
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// UserAccessCsvExportService
|
|
public async Task WriteSingleFileAsync(
|
|
IReadOnlyList<UserAccessEntry> entries,
|
|
string filePath,
|
|
CancellationToken ct,
|
|
bool mergePermissions = false)
|
|
{
|
|
if (mergePermissions)
|
|
{
|
|
var consolidated = PermissionConsolidator.Consolidate(entries);
|
|
// write consolidated CSV rows
|
|
}
|
|
// existing path unchanged
|
|
...
|
|
}
|
|
```
|
|
|
|
**ViewModel call sites** (in `ExportHtmlAsync` and `ExportCsvAsync`):
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding, MergePermissions);
|
|
await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);
|
|
```
|
|
|
|
### Pattern 4: Consolidated HTML By-User View Structure
|
|
|
|
**What:** When `mergePermissions = true`, the by-user table renders one row per `ConsolidatedPermissionEntry` with a new "Sites" column replacing the "Site" column. The by-site view toggle is hidden.
|
|
|
|
**Sites column rendering logic:**
|
|
- `LocationCount == 1`: render `entry.Locations[0].SiteTitle` as plain text (no badge/expand)
|
|
- `LocationCount >= 2`: render `<span class="badge sites-badge" onclick="toggleGroup('loc{idx}')">N sites</span>` followed by sub-rows `data-group="loc{idx}"` each containing a link to the location's SiteUrl with SiteTitle label
|
|
|
|
**Reuse of `toggleGroup()` JS:** The existing `toggleGroup(id)` function (line 276 in `UserAccessHtmlExportService.cs`) hides/shows rows by `data-group` attribute. Expandable location sub-lists use the same mechanism — each sub-row gets `data-group="loc{idx}"` and starts hidden.
|
|
|
|
**By-site view suppression:** When consolidation is ON, omit the "By Site" button and `view-site` div from the HTML entirely (simplest approach — no dead HTML). Alternatively, render the button as disabled. Omitting is cleaner.
|
|
|
|
**Column layout for consolidated by-user view:**
|
|
| Column | Source Field |
|
|
|--------|-------------|
|
|
| User | `UserDisplayName` (+ Guest badge if `IsExternalUser`) |
|
|
| Permission Level | `PermissionLevel` (+ high-priv icon if `IsHighPrivilege`) |
|
|
| Access Type | badge from `AccessType` |
|
|
| Granted Through | `GrantedThrough` |
|
|
| Sites | inline title OR `[N sites]` badge + expandable sub-list |
|
|
|
|
### Pattern 5: Consolidated CSV Format
|
|
|
|
**What:** When consolidation is ON, each `ConsolidatedPermissionEntry` becomes one row with a "Locations" column containing all site titles joined by `"; "`.
|
|
|
|
**Column layout for consolidated CSV:**
|
|
```
|
|
"User","User Login","Permission Level","Access Type","Granted Through","Locations","Location Count"
|
|
```
|
|
|
|
The "Locations" value: `string.Join("; ", entry.Locations.Select(l => l.SiteTitle))`.
|
|
|
|
This is a distinct schema from the flat CSV, so the consolidated path writes its own header line.
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Modifying the existing non-consolidated code path in any way** — the success criterion is byte-for-byte identical output when toggle is OFF. Touch nothing in the existing rendering branches.
|
|
- **Using a shared service for MergePermissions state** — CONTEXT.md says session-scoped property on the ViewModel, not a singleton service. Both ViewModels have independent instances; no cross-ViewModel coordination is needed because site-centric is a no-op.
|
|
- **Re-using `IsGroupByUser` to control by-site view in HTML** — `IsGroupByUser` controls the WPF DataGrid grouping only; the HTML export is stateless and renders both views based solely on the `mergePermissions` flag.
|
|
- **Adding a partial handler for `OnMergePermissionsChanged`** — no side effects are needed on toggle change (no view refresh required, HTML export is computed at export time).
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Permission grouping / merging | Custom LINQ grouping | `PermissionConsolidator.Consolidate()` | Already unit-tested with 9 cases; handles case-insensitivity, ordering, LocationInfo construction |
|
|
| Toggle state persistence | Session service, file-backed setting | `[ObservableProperty] bool _mergePermissions` defaults to false | Session-scoped = default bool initialization; no persistence infrastructure needed |
|
|
| Expandable sub-list JS | New JS function | Existing `toggleGroup()` in `UserAccessHtmlExportService.cs` | Already handles show/hide of `data-group` rows; locations sub-list uses same mechanism with `loc{idx}` IDs |
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Breaking the Byte-Identical Guarantee
|
|
**What goes wrong:** Any change to the non-consolidated HTML rendering path (even whitespace changes to `sb.AppendLine` calls) breaks the "byte-for-byte identical" success criterion.
|
|
**Why it happens:** Developers add a parameter and reorganize the method, inadvertently altering the existing branches.
|
|
**How to avoid:** Add the consolidated branch as a separate early-return path (`if (mergePermissions) { ... return; }`). Leave the existing `StringBuilder` building code completely untouched below that branch.
|
|
**Warning signs:** A test that captures the current output (`BuildHtml(entries, mergePermissions: false)`) and compares it character-by-character to the pre-toggle output fails.
|
|
|
|
### Pitfall 2: ID Collisions Between User Groups and Location Sub-lists
|
|
**What goes wrong:** The consolidated by-user view uses `ugrp{n}` IDs for user group headers and `loc{n}` IDs for expandable location sub-lists. If the counter resets between the two, `toggleGroup` clicks the wrong rows.
|
|
**Why it happens:** Two independent counters that both start at 0 use different prefixes — this is fine — but if a single counter is used for both, collisions occur.
|
|
**How to avoid:** Use a separate counter for location groups (`int locIdx = 0`) distinct from the user group counter.
|
|
|
|
### Pitfall 3: WriteAsync Signature Mismatch
|
|
**What goes wrong:** `UserAccessHtmlExportService.WriteAsync` calls `BuildHtml` internally. If `BuildHtml` gets the new `mergePermissions` parameter but `WriteAsync` doesn't pass it through, the HTML file ignores the toggle.
|
|
**Why it happens:** `WriteAsync` is the method called by the ViewModel; `BuildHtml` is the method being extended.
|
|
**How to avoid:** Extend `WriteAsync` with the same `bool mergePermissions = false` parameter and pass it to `BuildHtml`.
|
|
|
|
### Pitfall 4: CSV `BuildCsv` vs `WriteSingleFileAsync`
|
|
**What goes wrong:** The ViewModel calls `WriteSingleFileAsync` (single-file export). `BuildCsv` is per-user. Consolidation applies at the whole-collection level, so only `WriteSingleFileAsync` needs a consolidated path. Adding a consolidated path to `BuildCsv` (per-user) is incorrect — a consolidated row already spans users.
|
|
**Why it happens:** Developer sees `BuildCsv` and `WriteSingleFileAsync` and tries to add consolidation to both.
|
|
**How to avoid:** Add the consolidated branch only to `WriteSingleFileAsync`. `BuildCsv` is not called by the consolidation path. The per-user export (`WriteAsync`) is also not called by the ViewModel's export command (`ExportCsvAsync` calls `WriteSingleFileAsync` only).
|
|
|
|
### Pitfall 5: Localization Keys Missing from French .resx
|
|
**What goes wrong:** New string keys added to `Strings.resx` but not to `Strings.fr.resx` cause `TranslationSource.Instance[key]` to return an empty string in French locale.
|
|
**Why it happens:** Developers add only the default `.resx`.
|
|
**How to avoid:** Add both `audit.grp.export` and `chk.merge.permissions` to both `.resx` files in the same task.
|
|
|
|
## Code Examples
|
|
|
|
### Current Export Call Sites (ViewModel — to be extended)
|
|
|
|
```csharp
|
|
// ExportHtmlAsync — current (line 526 of UserAccessAuditViewModel.cs)
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding);
|
|
|
|
// ExportCsvAsync — current (line 494)
|
|
await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);
|
|
```
|
|
|
|
After Phase 16, becomes:
|
|
```csharp
|
|
await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding, MergePermissions);
|
|
await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);
|
|
```
|
|
|
|
### Existing toggleGroup JS (reuse as-is)
|
|
|
|
```javascript
|
|
// Source: UserAccessHtmlExportService.cs inline JS, line 276-279
|
|
function toggleGroup(id) {
|
|
var rows = document.querySelectorAll('tr[data-group="' + id + '"]');
|
|
var isHidden = rows.length > 0 && rows[0].style.display === 'none';
|
|
rows.forEach(function(r) { r.style.display = isHidden ? '' : 'none'; });
|
|
}
|
|
```
|
|
|
|
Note: the current source has `""` doubled quotes in the selector (`'tr[data-group=""' + id + '""]'`) — this is a C# string literal escaping artifact. When rendered to HTML the output is correct single-quoted attribute selectors. Location sub-list rows must use the same `data-group` attribute format.
|
|
|
|
### Consolidated HTML Sites Column — location sub-rows pattern
|
|
|
|
```html
|
|
<!-- 1 location: inline -->
|
|
<td>HR Site</td>
|
|
|
|
<!-- 2+ locations: badge + hidden sub-rows -->
|
|
<td><span class="badge" onclick="toggleGroup('loc3')" style="cursor:pointer">3 sites</span></td>
|
|
<!-- followed immediately after the main row: -->
|
|
<tr data-group="loc3" style="display:none">
|
|
<td colspan="5" style="padding-left:2em">
|
|
<a href="https://contoso.sharepoint.com/sites/hr">HR Site</a>
|
|
</td>
|
|
</tr>
|
|
<tr data-group="loc3" style="display:none">
|
|
<td colspan="5" style="padding-left:2em">
|
|
<a href="https://contoso.sharepoint.com/sites/fin">Finance Site</a>
|
|
</td>
|
|
</tr>
|
|
```
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| `BuildHtml(entries, branding)` | `BuildHtml(entries, mergePermissions, branding)` | Phase 16 | Callers must pass the flag |
|
|
| `WriteSingleFileAsync(entries, path, ct)` | `WriteSingleFileAsync(entries, path, ct, mergePermissions)` | Phase 16 | All callers (just the ViewModel) must pass the flag |
|
|
| No export option panel in XAML | Export Options GroupBox visible in both tabs | Phase 16 | Both XAML views change; both ViewModels gain the property |
|
|
|
|
## Open Questions
|
|
|
|
1. **`WriteAsync` vs `BuildHtml` parameter ordering**
|
|
- What we know: `WriteAsync` current signature is `(entries, filePath, ct, branding?)` — branding is already optional
|
|
- What's unclear: Should `mergePermissions` go before or after `branding` in the parameter list?
|
|
- Recommendation: Place `mergePermissions = false` after `ct` and before `branding?` so it's grouped with behavioral flags, not rendering decorations: `WriteAsync(entries, filePath, ct, mergePermissions = false, branding = null)`
|
|
|
|
2. **French translation for "Merge duplicate permissions"**
|
|
- What we know: French `.resx` exists and all existing keys have French equivalents
|
|
- What's unclear: Exact French translation (developer decision)
|
|
- Recommendation: Use "Fusionner les permissions en double" — consistent with existing terminology in the codebase
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | xUnit (project: `SharepointToolbox.Tests`) |
|
|
| Config file | `SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` |
|
|
| Quick run command | `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests" --no-build` |
|
|
| Full suite command | `dotnet test` |
|
|
|
|
### Phase Requirements → Test Map
|
|
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| RPT-03-a | `MergePermissions` defaults to `false` on ViewModel | unit | `dotnet test --filter "FullyQualifiedName~UserAccessAuditViewModelTests"` | ✅ (extend existing) |
|
|
| RPT-03-b | `BuildHtml(entries, false)` output is byte-identical to current output | unit | `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` | ✅ (extend existing) |
|
|
| RPT-03-c | `BuildHtml(entries, true)` includes consolidated rows and "Sites" column | unit | `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` | ✅ (extend existing) |
|
|
| RPT-03-d | `BuildHtml(entries, true)` with 2+ locations renders `[N sites]` badge | unit | `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` | ✅ (extend existing) |
|
|
| RPT-03-e | `BuildHtml(entries, true)` omits by-site view toggle | unit | `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests"` | ✅ (extend existing) |
|
|
| RPT-03-f | `WriteSingleFileAsync(entries, path, ct, false)` output unchanged | unit | `dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests"` | ✅ (extend existing) |
|
|
| RPT-03-g | `WriteSingleFileAsync(entries, path, ct, true)` writes consolidated CSV rows | unit | `dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests"` | ✅ (extend existing) |
|
|
|
|
### Sampling Rate
|
|
- **Per task commit:** `dotnet test --filter "FullyQualifiedName~UserAccessHtmlExportServiceTests|FullyQualifiedName~UserAccessCsvExportServiceTests|FullyQualifiedName~UserAccessAuditViewModelTests" --no-build`
|
|
- **Per wave merge:** `dotnet test`
|
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
None — existing test infrastructure covers all phase requirements. No new test files needed; existing test files are extended with new test methods.
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- Direct inspection of `UserAccessHtmlExportService.cs` — full rendering pipeline, `toggleGroup()` JS, `BuildHtml`/`WriteAsync` signatures
|
|
- Direct inspection of `UserAccessCsvExportService.cs` — `BuildCsv`, `WriteSingleFileAsync`, `WriteAsync` signatures
|
|
- Direct inspection of `UserAccessAuditViewModel.cs` — `[ObservableProperty]` pattern, `ExportCsvAsync`, `ExportHtmlAsync` call sites
|
|
- Direct inspection of `PermissionsViewModel.cs` — confirms independent ViewModel, no shared state service
|
|
- Direct inspection of `UserAccessAuditView.xaml` — Scan Options GroupBox pattern at lines 199-210
|
|
- Direct inspection of `PermissionsView.xaml` — Display Options GroupBox pattern and left panel structure
|
|
- Direct inspection of `PermissionConsolidator.cs` + `ConsolidatedPermissionEntry.cs` + `LocationInfo.cs` — complete Phase 15 API
|
|
- Direct inspection of `PermissionConsolidatorTests.cs` — 9 test cases confirming consolidator behavior
|
|
- Direct inspection of `Strings.resx` — existing localization key naming convention
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- CommunityToolkit.Mvvm `[ObservableProperty]` source generation behavior — confirmed by existing usage of `_includeInherited`, `_scanFolders`, `_mergePermissions` pattern throughout codebase
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — all dependencies already in the project; no new libraries
|
|
- Architecture: HIGH — all patterns directly observed in existing code
|
|
- Pitfalls: HIGH — derived from direct reading of the code paths being modified
|
|
- Test map: HIGH — existing test file locations and xUnit patterns confirmed
|
|
|
|
**Research date:** 2026-04-09
|
|
**Valid until:** 2026-05-09 (stable codebase, no fast-moving dependencies)
|