From af2177046f58e2a7945146bdaf3abaf50d1d5926 Mon Sep 17 00:00:00 2001 From: Dev Date: Fri, 3 Apr 2026 14:27:51 +0200 Subject: [PATCH] docs(phase-05): research distribution and hardening phase Verified single-file publish works with IncludeNativeLibrariesForSelfExtract, documented 25+ FR diacritic gaps, and mapped retry/pagination test coverage gaps. Co-Authored-By: Claude Sonnet 4.6 --- .../05-RESEARCH.md | 438 ++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 .planning/phases/05-distribution-and-hardening/05-RESEARCH.md diff --git a/.planning/phases/05-distribution-and-hardening/05-RESEARCH.md b/.planning/phases/05-distribution-and-hardening/05-RESEARCH.md new file mode 100644 index 0000000..0ff2f59 --- /dev/null +++ b/.planning/phases/05-distribution-and-hardening/05-RESEARCH.md @@ -0,0 +1,438 @@ +# Phase 5: Distribution and Hardening - Research + +**Researched:** 2026-04-03 +**Domain:** .NET 10 WPF single-file publishing, localization completeness, reliability verification +**Confidence:** HIGH + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| FOUND-11 | Self-contained single EXE distribution — no .NET runtime dependency for end users | dotnet publish -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true produces single EXE | + + +--- + +## Summary + +Phase 5 delivers the final shipping artifact: a self-contained Windows EXE that runs without any pre-installed .NET runtime, a verified reliable behavior against SharePoint's 5,000-item threshold and throttling, and a complete French locale with proper diacritics. + +The core publish mechanism is already proven to work: `dotnet publish -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true` was tested against the current codebase and produces exactly two output files — `SharepointToolbox.exe` (~201 MB, self-extracting) and `SharepointToolbox.pdb`. The EXE bundles all managed assemblies and the WPF native runtime DLLs (D3DCompiler, PresentationNative, wpfgfx, etc.) by extraction into `%TEMP%/.net` on first run. The `msalruntime.dll` WAM broker is also bundled. No additional flags beyond `PublishSingleFile + IncludeNativeLibrariesForSelfExtract` are needed. + +The French locale has a documented quality gap: approximately 25 Phase 4 strings in `Strings.fr.resx` are missing required French diacritics (e.g., `"Bibliotheque"` instead of `"Bibliothèque"`, `"Creer"` instead of `"Créer"`, `"echoue"` instead of `"échoué"`). These strings display in-app when the user switches to French — they are not missing keys but incorrect values. All 177 keys exist in both EN and FR files (parity is 100%), so the task is correction not addition. + +The retry helper (`ExecuteQueryRetryHelper`) and pagination helper (`SharePointPaginationHelper`) are implemented but have zero unit tests. The throttling behavior and 5,000-item pagination are covered only by live CSOM context tests marked Skip. Phase 5 must add tests that exercise these code paths without live SharePoint — using a fake that throws a 429-like exception for retry, and an in-memory list of items for pagination. + +**Primary recommendation:** Implement as three parallel workstreams — (1) add `PublishSingleFile` + `IncludeNativeLibrariesForSelfExtract` to csproj and create a publish profile, (2) fix the 25 diacritic-missing FR strings and add a locale completeness test, (3) add unit tests for `ExecuteQueryRetryHelper` and `SharePointPaginationHelper`. + +--- + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| .NET SDK publish CLI | 10.0.200 | Self-contained single-file packaging | Built-in, no extra tool | +| xUnit | 2.9.3 | Unit tests for retry/pagination helpers | Already in test project | +| Moq | 4.20.72 | Mock `ClientContext` for retry test isolation | Already in test project | + +### No New Dependencies +Phase 5 adds zero NuGet packages. All capability is already in the SDK and test infrastructure. + +**Publish command (verified working):** +```bash +dotnet publish SharepointToolbox/SharepointToolbox.csproj \ + -c Release \ + -r win-x64 \ + --self-contained true \ + -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -o ./publish +``` + +**Output (verified):** +- `SharepointToolbox.exe` — ~201 MB self-contained EXE +- `SharepointToolbox.pdb` — symbols (can be excluded in release by setting `none`) + +--- + +## Architecture Patterns + +### Recommended Project Structure Changes + +``` +SharepointToolbox/ +├── SharepointToolbox.csproj # Add publish properties +└── Properties/ + └── PublishProfiles/ + └── win-x64.pubxml # Reusable publish profile (optional but clean) +``` + +``` +SharepointToolbox.Tests/ +└── Services/ + ├── ExecuteQueryRetryHelperTests.cs # NEW — retry/throttling tests + └── SharePointPaginationHelperTests.cs # NEW — pagination tests +``` + +### Pattern 1: Self-Contained Single-File Publish Configuration + +**What:** Set publish properties in .csproj so `dotnet publish` requires no extra flags +**When to use:** Preferred over CLI-only flags — reproducible builds, CI compatibility + +```xml + + + true + true + win-x64 + true + + + +``` + +**Important:** `PublishSingleFile` must NOT be combined with `PublishTrimmed=true`. The project already has `false` — this is correct and must remain false (PnP.Framework and MSAL use reflection). + +### Pattern 2: Testing `ExecuteQueryRetryHelper` Without Live CSOM + +**What:** Verify retry behavior by injecting a fake exception into `ClientContext.ExecuteQueryAsync` +**When to use:** `ClientContext` cannot be instantiated without a live SharePoint URL + +The existing `ExecuteQueryRetryHelper.ExecuteQueryRetryAsync` takes a `ClientContext` parameter — it cannot be directly unit tested without either (a) a live context or (b) an abstraction layer. The cleanest approach without redesigning the helper is to **extract an `ISharePointExecutor` interface** that wraps `ctx.ExecuteQueryAsync()` and inject a fake that throws then succeeds. + +```csharp +// Proposed thin abstraction — no CSOM dependency in tests +public interface ISharePointExecutor +{ + Task ExecuteAsync(CancellationToken ct = default); +} + +// Production adapter +public class ClientContextExecutor : ISharePointExecutor +{ + private readonly ClientContext _ctx; + public ClientContextExecutor(ClientContext ctx) => _ctx = ctx; + public Task ExecuteAsync(CancellationToken ct) => _ctx.ExecuteQueryAsync(); +} +``` + +Then `ExecuteQueryRetryHelper.ExecuteQueryRetryAsync` gains an overload accepting `ISharePointExecutor` — the `ClientContext` overload becomes a convenience wrapper. + +Alternatively (simpler, avoids interface): test `IsThrottleException` directly (it is `private static` — make it `internal static` + `InternalsVisibleTo` the test project), and test the retry loop indirectly via a stub subclass approach. + +**Simplest path requiring minimal refactoring:** Change `IsThrottleException` to `internal static` and add `InternalsVisibleTo` (established project pattern from Phase 2 `DeriveAdminUrl`). Test the exception classification directly. For retry loop coverage, add an integration test that constructs a real exception with "429" in the message. + +### Pattern 3: Testing `SharePointPaginationHelper` Without Live CSOM + +**What:** The `GetAllItemsAsync` method uses `ctx.ExecuteQueryAsync()` internally — cannot be unit tested without a live context +**When to use:** Pagination logic must be verified without a live tenant + +The pagination helper's core correctness is in `BuildPagedViewXml` (private static). The approach: +1. Make `BuildPagedViewXml` internal static (parallel to `DeriveAdminUrl` precedent) +2. Unit test the XML injection logic directly +3. Mark the live pagination test as Skip with a clear comment explaining the 5,000-item guarantee comes from the `ListItemCollectionPosition` loop + +For the success criterion "scan against a library with more than 5,000 items returns complete, correct results", the verification is a **manual smoke test** against a real tenant — it cannot be automated without a live SharePoint environment. + +### Pattern 4: Locale Completeness Automated Test + +**What:** Assert that every key in `Strings.resx` has a non-empty, non-bracketed value in `Strings.fr.resx` +**When to use:** Prevents future regressions where new EN keys are added without FR equivalents + +```csharp +// Source: .NET ResourceManager pattern — verified working in test suite +[Fact] +public void AllEnKeys_HaveNonEmptyFrTranslation() +{ + var enManager = new ResourceManager("SharepointToolbox.Localization.Strings", + typeof(Strings).Assembly); + var frCulture = new CultureInfo("fr-FR"); + + // Get all EN keys by switching to invariant and enumerating + var resourceSet = enManager.GetResourceSet(CultureInfo.InvariantCulture, true, true); + foreach (DictionaryEntry entry in resourceSet!) + { + var key = (string)entry.Key; + var frValue = enManager.GetString(key, frCulture); + Assert.False(string.IsNullOrWhiteSpace(frValue), + $"Key '{key}' has no French translation."); + Assert.DoesNotContain("[", frValue!, + $"Key '{key}' returns bracketed fallback in French."); + } +} +``` + +**Note:** The existing `TranslationSourceTests.Indexer_ReturnsFrOrFallback_AfterSwitchToFrFR` only checks one key (`app.title`). This new test checks all 177 keys exhaustively. + +### Anti-Patterns to Avoid + +- **Adding `PublishTrimmed=true`:** PnP.Framework and MSAL both use reflection; trimming will silently break authentication and CSOM calls at runtime. The project already has `false` — keep it. +- **Framework-dependent publish:** `--self-contained false` requires the target machine to have .NET 10 installed. FOUND-11 requires no runtime dependency. +- **Omitting `IncludeNativeLibrariesForSelfExtract=true`:** Without this flag, 6 WPF native DLLs (~8 MB total) land alongside the EXE, violating the "single file" contract. The publish is otherwise identical. +- **Using `win-x86` RID:** The app targets 64-bit Windows. Using `win-x86` would build but produce a 32-bit EXE that cannot use more than 4 GB RAM during large library scans. +- **Correcting FR strings via code string concatenation:** Fix diacritics in the `.resx` XML file directly. Do not work around them in C# code. The ResourceManager handles UTF-8 correctly. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Self-contained EXE packaging | Custom bundler / NSIS installer | `dotnet publish --self-contained -p:PublishSingleFile=true` | SDK-native, zero extra tooling | +| Native library bundling | Script to ZIP DLLs | `IncludeNativeLibrariesForSelfExtract=true` | SDK handles extraction to `%TEMP%/.net` | +| Key enumeration for locale completeness | Parsing resx XML manually | `ResourceManager.GetResourceSet(InvariantCulture, true, true)` | Returns all keys as `DictionaryEntry` | +| Retry/backoff logic | Custom retry loops per call site | `ExecuteQueryRetryHelper.ExecuteQueryRetryAsync` (already exists) | Already implemented with exponential back-off | +| Pagination loop | Custom CAML page iteration per feature | `SharePointPaginationHelper.GetAllItemsAsync` (already exists) | Already handles `ListItemCollectionPosition` | + +**Key insight:** Every mechanism needed for Phase 5 already exists in the codebase. This phase is verification + correction + packaging, not feature construction. + +--- + +## Common Pitfalls + +### Pitfall 1: WPF Native DLLs Left Outside the Bundle +**What goes wrong:** Running `dotnet publish -p:PublishSingleFile=true` without `IncludeNativeLibrariesForSelfExtract=true` leaves `D3DCompiler_47_cor3.dll`, `PenImc_cor3.dll`, `PresentationNative_cor3.dll`, `vcruntime140_cor3.dll`, and `wpfgfx_cor3.dll` as loose files next to the EXE. +**Why it happens:** By design — the SDK bundles managed DLLs but not native runtime DLLs by default. +**How to avoid:** Always set `true` in the csproj or publish command. +**Warning signs:** Publish output folder contains any `.dll` files besides `msalruntime.dll` (WAM broker — bundled with IncludeNativeLibraries). + +**Verified:** In test publish without the flag: 6 loose DLLs. With the flag: zero loose DLLs (only EXE + PDB). + +### Pitfall 2: First-Run Extraction Delay and Antivirus False Positives +**What goes wrong:** On the first run on a clean machine, `IncludeNativeLibrariesForSelfExtract` causes the EXE to extract ~8 MB of native DLLs into `%TEMP%\.net\\`. This can trigger antivirus software and cause a 2-5 second startup delay. +**Why it happens:** The single-file extraction mechanism writes files to disk before the CLR can load them. +**How to avoid:** This is expected behavior. No mitigation needed for this tool. If needed: `DOTNET_BUNDLE_EXTRACT_BASE_DIR` environment variable can redirect extraction. +**Warning signs:** App appears to hang for 5+ seconds on first launch on a clean machine — this is the extraction, not a bug. + +### Pitfall 3: `PublishTrimmed=true` Silent Runtime Failures +**What goes wrong:** PnP.Framework and MSAL use reflection to resolve types at runtime. Trimming removes types the linker cannot statically trace, causing `FileNotFoundException` or `MissingMethodException` at runtime on a clean machine — not during publish. +**Why it happens:** Trimming is an optimization that removes dead code but cannot analyze reflection-based type loading. +**How to avoid:** The project already has `false`. Never change this. +**Warning signs:** App works in Debug but crashes on clean machine after trimmed publish. + +### Pitfall 4: French Strings Display as Unaccented Text +**What goes wrong:** Phase 4 introduced ~25 FR strings with missing diacritics. When the user switches to French, strings like "Bibliothèque source" display as "Bibliotheque source". This is not a fallback — the wrong French text IS returned from the ResourceManager. +**Why it happens:** The strings were written without diacritics during Phase 4 localization work. +**How to avoid:** Fix each affected string in `Strings.fr.resx` and add the exhaustive locale completeness test. +**Warning signs:** Any FR string containing `e` where `é`, `è`, `ê` is expected. Full list documented in Code Examples section. + +### Pitfall 5: Retry Helper Has No Unit Test Coverage +**What goes wrong:** If `IsThrottleException` has a bug (wrong string detection), throttled requests will fail without retry. This is invisible without a test. +**Why it happens:** The helper was implemented in Phase 1 but no retry-specific test was created (test scaffolds focused on services, not helpers). +**How to avoid:** Add `ExecuteQueryRetryHelperTests.cs` with exception classification tests and a live-stub retry loop test. + +### Pitfall 6: `Assembly.GetExecutingAssembly().Location` Returns Empty String in Single-File +**What goes wrong:** If any code uses `Assembly.Location` to build a file path, it returns `""` in a single-file EXE. +**Why it happens:** Official .NET single-file limitation. +**How to avoid:** Code audit confirmed — the codebase uses `GetManifestResourceStream` (compatible) and `AppContext.BaseDirectory` (compatible). No `Assembly.Location` usage found. +**Warning signs:** Any path construction using `Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)`. + +--- + +## Code Examples + +### Single-File Publish csproj Configuration + +```xml + + +true +true +win-x64 +true +``` + +### Verified Publish Command + +```bash +# Run from SharepointToolbox/ project directory +# Produces: ./publish/SharepointToolbox.exe (~201 MB) + SharepointToolbox.pdb +dotnet publish -c Release -r win-x64 --self-contained true \ + -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -o ./publish +``` + +### FR Strings Requiring Diacritic Correction + +The following strings in `Strings.fr.resx` have incorrect values (missing accents). Each line shows: key → current wrong value → correct French value: + +``` +transfer.sourcelibrary "Bibliotheque source" → "Bibliothèque source" +transfer.destlibrary "Bibliotheque destination" → "Bibliothèque destination" +transfer.mode.move "Deplacer" → "Déplacer" +transfer.conflict.overwrite "Ecraser" → "Écraser" +transfer.start "Demarrer le transfert" → "Démarrer le transfert" +transfer.nofiles "Aucun fichier a transferer" → "Aucun fichier à transférer." +bulkmembers.preview "Apercu (...)" → "Aperçu (...)" +bulksites.execute "Creer les sites" → "Créer les sites" +bulksites.preview "Apercu (...)" → "Aperçu (...)" +bulksites.owners "Proprietaires" → "Propriétaires" +folderstruct.execute "Creer les dossiers" → "Créer les dossiers" +folderstruct.preview "Apercu ({0} dossiers a creer)" → "Aperçu ({0} dossiers à créer)" +folderstruct.library "Bibliotheque cible" → "Bibliothèque cible" +templates.list "Modeles enregistres" → "Modèles enregistrés" +templates.opt.libraries "Bibliotheques" → "Bibliothèques" +templates.opt.folders (ok: "Dossiers") +templates.opt.permissions (ok: "Groupes de permissions") +bulk.confirm.proceed "Continuer" → OK (no diacritic needed) +bulk.result.success "Termine : {0} reussis, {1} echoues" → "Terminé : {0} réussis, {1} échoués" +bulk.result.allfailed "Les {0} elements ont echoue." → "Les {0} éléments ont échoué." +bulk.result.allsuccess "Les {0} elements ont ete traites avec succes." → "Les {0} éléments ont été traités avec succès." +bulk.exportfailed "Exporter les elements echoues" → "Exporter les éléments échoués" +bulk.retryfailed "Reessayer les echecs" → "Réessayer les échecs" +bulk.validation.invalid "{0} lignes contiennent des erreurs. Corrigez et reimportez." → "...réimportez." +bulk.csvimport.title "Selectionner un fichier CSV" → "Sélectionner un fichier CSV" +folderbrowser.title "Selectionner un dossier" → "Sélectionner un dossier" +folderbrowser.select "Selectionner" → "Sélectionner" +``` + +**Note on `templates.*` keys:** Several templates keys also lack accents: `templates.capture "Capturer un modele"` → `"Capturer un modèle"`, `templates.apply "Appliquer le modele"` → `"Appliquer le modèle"`, `templates.name "Nom du modele"` → `"Nom du modèle"`, etc. + +### ExecuteQueryRetryHelper — `IsThrottleException` Unit Test Pattern + +```csharp +// Source: established project test pattern (InternalsVisibleTo + internal static) +// Make IsThrottleException internal static in ExecuteQueryRetryHelper +[Theory] +[InlineData("The request has been throttled — 429")] +[InlineData("Service unavailable 503")] +[InlineData("SharePoint has throttled your request")] +public void IsThrottleException_ReturnsTrueForThrottleMessages(string message) +{ + var ex = new Exception(message); + Assert.True(ExecuteQueryRetryHelper.IsThrottleException(ex)); +} + +[Fact] +public void IsThrottleException_ReturnsFalseForNonThrottleException() +{ + var ex = new Exception("File not found"); + Assert.False(ExecuteQueryRetryHelper.IsThrottleException(ex)); +} +``` + +### SharePointPaginationHelper — `BuildPagedViewXml` Unit Test Pattern + +```csharp +// Make BuildPagedViewXml internal static +[Fact] +public void BuildPagedViewXml_EmptyInput_ReturnsViewWithRowLimit() +{ + var result = SharePointPaginationHelper.BuildPagedViewXml(null, rowLimit: 2000); + Assert.Equal("2000", result); +} + +[Fact] +public void BuildPagedViewXml_ExistingRowLimit_Replaces() +{ + var input = "100"; + var result = SharePointPaginationHelper.BuildPagedViewXml(input, rowLimit: 2000); + Assert.Equal("2000", result); +} +``` + +--- + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Separate installer (MSI/NSIS) | Single-file self-contained EXE | .NET 5+ | No installer needed — xcopy deployment | +| Framework-dependent deploy | Self-contained deploy | .NET Core 3.1+ | No runtime prereq on target machine | +| Native DLLs always loose | `IncludeNativeLibrariesForSelfExtract=true` | .NET 5+ | True single-EXE for WPF | +| Manual publish profile | `` in csproj | .NET 5+ | Reproducible via `dotnet publish` | + +**Deprecated/outdated:** +- ClickOnce: Still exists but not used here — requires IIS/file share hosting, adds update mechanism this project doesn't need. +- MSIX packaging: Requires Developer Mode or certificate signing — overkill for an admin tool distributed by the developer directly. + +--- + +## Open Questions + +1. **PDB embedding vs. separate file** + - What we know: `embedded` merges PDB into the EXE; `none` strips symbols entirely. + - What's unclear: User preference for crash debugging — does the admin want to be able to debug crashes post-deploy? + - Recommendation: Default to keeping the separate PDB (current behavior). If release packaging requires it, `none` is the correct property. + +2. **WAM broker (`msalruntime.dll`) on clean machines** + - What we know: With `IncludeNativeLibrariesForSelfExtract=true`, `msalruntime.dll` is bundled into the EXE and extracted on first run. The interactive MSAL login (browser/WAM) uses this DLL. + - What's unclear: Whether WAM authentication works correctly when the runtime is extracted vs. being a loose file. Not testable without a clean machine. + - Recommendation: Flag for human smoke test — launch on a clean Windows 10/11 machine, authenticate to a tenant, verify the browser/WAM login flow completes. + +3. **`EnableCompressionInSingleFile` tradeoff** + - What we know: Setting this true reduces EXE size (201 MB → potentially ~130 MB) but adds ~1-3 second startup decompression delay. + - What's unclear: User tolerance for startup delay vs. distribution size. + - Recommendation: Do not set — the 201 MB EXE is acceptable for an admin tool. Compression adds startup complexity with minimal distribution benefit for this use case. + +--- + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | xUnit 2.9.3 | +| Config file | SharepointToolbox.Tests/SharepointToolbox.Tests.csproj | +| Quick run command | `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --no-build -v quiet` | +| Full suite command | `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj -v quiet` | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| FOUND-11-a | `PublishSingleFile` + `IncludeNativeLibrariesForSelfExtract` produces single EXE | smoke (manual) | `dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o /tmp/pub && ls /tmp/pub/*.dll \| wc -l` (expect 0) | ❌ Wave 0 | +| FOUND-11-b | App launches on clean machine with no .NET runtime | manual smoke | N/A — requires clean VM | manual-only | +| SC-2-retry | `ExecuteQueryRetryHelper.IsThrottleException` classifies 429/503 correctly | unit | `dotnet test --filter "ExecuteQueryRetryHelper"` | ❌ Wave 0 | +| SC-2-retry | Retry loop reports progress and eventually throws after MaxRetries | unit | `dotnet test --filter "ExecuteQueryRetryHelper"` | ❌ Wave 0 | +| SC-3-fr | All 177 EN keys have non-empty, non-bracketed FR values | unit | `dotnet test --filter "LocaleCompleteness"` | ❌ Wave 0 | +| SC-3-fr | No diacritic-missing strings appear when language=FR | manual smoke | N/A — visual inspection | manual-only | +| SC-4-paginat | `BuildPagedViewXml` correctly injects and replaces RowLimit | unit | `dotnet test --filter "SharePointPagination"` | ❌ Wave 0 | +| SC-4-paginat | Scan against 5,000+ item library returns complete results | manual smoke | N/A — requires live tenant | manual-only | + +### Sampling Rate +- **Per task commit:** `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --no-build -v quiet` +- **Per wave merge:** `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj -v quiet` +- **Phase gate:** Full suite green before `/gsd:verify-work` + manual smoke checklist + +### Wave 0 Gaps +- [ ] `SharepointToolbox.Tests/Services/ExecuteQueryRetryHelperTests.cs` — covers SC-2-retry +- [ ] `SharepointToolbox.Tests/Services/SharePointPaginationHelperTests.cs` — covers SC-4-paginat +- [ ] `SharepointToolbox.Tests/Localization/LocaleCompletenessTests.cs` — covers SC-3-fr +- [ ] `ExecuteQueryRetryHelper.IsThrottleException` must be changed from `private static` to `internal static` +- [ ] `SharePointPaginationHelper.BuildPagedViewXml` must be changed from `private static` to `internal static` +- [ ] `InternalsVisibleTo("SharepointToolbox.Tests")` already in `AssemblyInfo.cs` — no change needed + +--- + +## Sources + +### Primary (HIGH confidence) +- Official .NET docs: https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview — single-file publish behavior, API incompatibilities, `IncludeNativeLibrariesForSelfExtract` +- Live publish test (2026-04-03): ran `dotnet publish` on actual codebase — confirmed single EXE output with and without `IncludeNativeLibrariesForSelfExtract` +- Direct code inspection: `ExecuteQueryRetryHelper.cs`, `SharePointPaginationHelper.cs`, `Strings.resx`, `Strings.fr.resx`, `SharepointToolbox.csproj` + +### Secondary (MEDIUM confidence) +- Microsoft Q&A: https://learn.microsoft.com/en-us/answers/questions/990342/wpf-publishing-application-into-single-exe-file — confirmed WPF native DLL bundling behavior +- MSAL WAM docs: https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/desktop-mobile/wam — msalruntime.dll behavior with broker + +### Tertiary (LOW confidence) +- None + +--- + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — publish tested live against actual codebase +- Architecture: HIGH — based on direct code inspection + official docs +- Pitfalls: HIGH — pitfall 1 verified experimentally; others from official docs + established project patterns +- FR string gaps: HIGH — direct inspection of Strings.fr.resx, enumerated 25+ affected keys + +**Research date:** 2026-04-03 +**Valid until:** 2026-07-03 (stable .NET tooling; 90 days reasonable)