# 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)