From 4143b62606268ca611c5c2999cce11beaca9ceb5 Mon Sep 17 00:00:00 2001
From: nicolasestrem <103498114+nicolasestrem@users.noreply.github.com>
Date: Sun, 9 Aug 2026 21:25:40 +0200
Subject: [PATCH 1/4] docs: exclude publish-* dirs, document WPF Timeline shell
and crash fixes
---
.gitignore | 2 ++
CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++------
2 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/.gitignore b/.gitignore
index 50124dc..a51007f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ src/packages/
# Release artifact directory used by the /release skill (a ~69 MB self-contained exe).
publish/
+# WPF and UI test publish directories (self-contained executables, ~70 MB each).
+publish-*/
# dotnet test output: .trx logs, and the crash/hang dumps --blame-hang writes, which run to
# hundreds of megabytes each.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb351f9..93e2b42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,47 @@ version numbers, because that is what those releases were called.
## [Unreleased]
+### Added — WPF Timeline + Compare shell (side-by-side with WinForms)
+
+A new WPF application (`WinRestoreKit.Wpf`) ships alongside the existing WinForms app, implementing the
+approved Timeline + Comparison Workspace design direction. It does not replace WinForms yet; both run
+from the same solution and share the same Core engine and Application layer.
+
+**Timeline is the home experience.** Every snapshot and failed attempt on this PC appears as a card on a
+point-in-time rail, newest first. Selecting a verified snapshot opens comparison; failed and unreadable
+entries open their diagnostic reason. Arrow keys navigate the list and every custom visual exposes UI
+Automation names, roles, and states.
+
+**Compare shows evidence before restore.** Selecting a snapshot compares each module against the current
+PC and reports honest state: captured, not captured, or changed. You build a restore set by selecting
+whole modules, then move to a Confirm stage that reviews impact, warnings, and application-close
+consent before any restore starts.
+
+**Backup, Progress, and Results are migrated.** The backup workspace offers scope presets, destination,
+compression, and validation. Progress shows stage, percent, throughput, byte counts, errors, warnings,
+and a live log. Results present a severity summary with per-module outcomes.
+
+**Visual system.** Neutral Windows surfaces, one mineral-blue action colour, one restrained coral
+warning, Light/Dark/Follow-system themes, Segoe UI Variable typography, IBM Plex Mono for technical
+identifiers, 6–10 px corner radii, flat panes, and visible keyboard focus throughout. Raw icon-enum
+text (e.g. `ErrorCircle`) was eliminated at its source: status glyphs are vector paths keyed by tone,
+never text.
+
+**Application layer extraction.** Orchestration, backup presets, scope groups, run UI, snapshot
+services, and update/theme services moved from the WinForms project into a new
+`WinRestoreKit.Application` library shared by both shells. Core remains unchanged in its isolation
+guarantee: no WinForms or WPF dependency can compile against it.
+
+### Fixed — WPF crashes on snapshot creation and About navigation
+
+Two read-only property bindings in the redesigned WPF views caused `InvalidOperationException` crashes:
+
+- **About page** bound `Run.Text` to the read-only `CurrentVersion` property (TwoWay by default).
+- **Progress view** bound `ProgressBar.Value` to the read-only `Percent` property (TwoWay by default).
+
+Both are fixed with `Mode=OneWay`. Regression tests render each view through the layout dispatcher to
+prove the crash path no longer fires.
+
### Design
- Started a three-direction visual identity and WinUI 3 Home-screen exploration around the
@@ -17,12 +58,6 @@ version numbers, because that is what those releases were called.
calmer spacing. Logo exploration remains unselected and no runtime or backup-format behavior has
changed.
-### Changed
-
-- Rebuilt the app shell and all primary views with the Industry design system: bundled Barlow, Barlow Condensed, and IBM Plex Mono typography; Voltage, Flux, and Follow system palettes; blueprint frames; icon rail navigation; and a dedicated progress view.
-- Added snapshot display names, selectable destination folders, Fast and Max archive compression, archive-backed restore discovery, live registry drift detection, rich backup progress metrics, and safe pause or cancel controls.
-- Reworked backup, restore, History, Home, and About around real manifest and module data. Existing backup folders and frozen manifest keys remain compatible.
-
## [0.0.1] - 2026-08-02
### Changed
From 34a9bab938400065c389a786bda053092e322813 Mon Sep 17 00:00:00 2001
From: nicolasestrem <103498114+nicolasestrem@users.noreply.github.com>
Date: Sun, 9 Aug 2026 21:26:16 +0200
Subject: [PATCH 2/4] feat: WPF Timeline + Compare shell, Application layer
extraction, crash fixes
Add a new WPF application (WinRestoreKit.Wpf) implementing the approved
Timeline + Comparison Workspace design, running side-by-side with the
existing WinForms shell. Both share the same Core engine and a new
WinRestoreKit.Application library.
Architecture:
- WinRestoreKit.Application: orchestration, backup presets, scope groups,
snapshot event catalog, comparison service, update/theme services extracted
from the WinForms project and shared by both shells
- WinRestoreKit.Wpf: Timeline home, Compare/Confirm restore workflow, Backup
workspace with presets, Progress with live metrics, Results, Advanced
History, Settings, About, App Restore dialog
- WinRestoreKit.Core: unchanged isolation guarantee (no UI dependency)
- WinRestoreKit (WinForms): updated to reference Application layer
Visual system:
- Neutral Windows surfaces, mineral-blue actions, restrained coral warnings
- Light/Dark/Follow-system themes with shared token set (~35 colours each)
- Segoe UI Variable + IBM Plex Mono typography
- Vector status glyphs (eliminates raw ErrorCircle enum text at source)
- Full implicit control templates, visible keyboard focus, accessible names
- 6-10px radii, flat panes, no gradients/glow/blur
Fixed:
- AboutView: Run.Text bound CurrentVersion read-only (TwoWay default) -> Mode=OneWay
- ProgressWorkspaceView: ProgressBar.Value bound Percent read-only -> Mode=OneWay
- Both crash with InvalidOperationException on view render; regression tests added
Tests: 980 passed, 0 failed, 0 skipped (Release)
Build: 0 warnings, 0 errors
---
assets/WinRestoreKit-one-pager.html | 567 ++++++
.../2026-08-09-backup-progress-results.md | 1321 ++++++++++++
.../2026-08-09-compare-confirm-restore.md | 1812 +++++++++++++++++
...-timeline-compare-wpf-migration-roadmap.md | 77 +
.../plans/2026-08-09-timeline-event-model.md | 906 +++++++++
...-08-09-wpf-cutover-release-verification.md | 1407 +++++++++++++
...-08-09-wpf-foundation-application-shell.md | 1454 +++++++++++++
...08-09-timeline-compare-wpf-shell-design.md | 353 ++++
.../AppRestore/AppRestoreService.cs | 270 +++
.../Backup/BackupCompletionPublisher.cs | 44 +
.../Backup/BackupPresets.cs | 13 +
.../Backup}/ScopeGroups.cs | 72 +-
.../Comparison/ComparisonState.cs | 10 +
.../Comparison/ModuleComparison.cs | 27 +
.../Comparison/SnapshotComparisonService.cs | 261 +++
.../Modules/BackupModuleCatalog.cs | 14 +
.../Modules/BackupModuleRegistration.cs | 20 +
.../BackupRestoreOrchestrator.cs | 22 +-
.../Orchestration/IRunUi.cs | 16 +-
.../Orchestration/RunControl.cs | 0
.../Orchestration/RunCoordinator.cs | 0
.../Properties/AssemblyInfo.cs | 5 +
.../Results/RunSummary.cs | 17 +-
.../Settings}/BackupRootRegistry.cs | 0
.../Settings/IThemeSettings.cs | 8 +
.../Settings/RegistryThemeSettings.cs | 46 +
.../Settings/ThemeMode.cs | 9 +
.../Snapshots}/BackupFolders.cs | 199 +-
.../Snapshots/SnapshotEvent.cs | 42 +
.../Snapshots/SnapshotEventCatalog.cs | 199 ++
.../Snapshots/SnapshotEventKind.cs | 10 +
.../Snapshots/SnapshotPayloadPreparation.cs | 42 +
.../SnapshotPayloadPreparationService.cs | 45 +
.../Updates/IUpdateCheckService.cs | 10 +
.../Updates/UpdateCheckResult.cs | 22 +
.../Updates/UpdateCheckService.cs | 112 +
.../Updates/UpdateVerdict.cs | 10 +
.../Updates/VersionInfo.cs | 27 +
.../WinRestoreKit.Application.csproj | 14 +
src/WinRestoreKit.Core/Conf/AppStoreApps.cs | 49 +-
src/WinRestoreKit.Core/Conf/FileModule.cs | 34 +-
src/WinRestoreKit.Core/Conf/FolderModule.cs | 35 +-
src/WinRestoreKit.Core/Conf/WPowerPlans.cs | 14 +-
src/WinRestoreKit.Core/Results/RegFile.cs | 43 +-
.../Results/RestoreContents.cs | 28 +-
src/WinRestoreKit.Core/Results/RestoreLog.cs | 33 +-
.../Results/SnapshotGate.cs | 17 +-
.../WinRestoreKit.Core.csproj | 2 +
.../AdvancedHistoryViewModelTests.cs | 88 +
.../AppRestoreDialogTests.cs | 690 +------
.../AppRestoreServiceTests.cs | 104 +
.../ApplicationBoundaryTests.cs | 34 +
.../ArchiveProgressTests.cs | 6 +-
.../BackupCompletionPublisherTests.cs | 65 +
.../BackupDestinationContainmentTests.cs | 6 +-
.../BackupDestinationLifecycleTests.cs | 26 +-
.../BackupFoldersReadTests.cs | 23 +-
.../BackupModuleCatalogTests.cs | 24 +
.../BackupOutputPathTests.cs | 94 +
.../BackupPageViewTests.cs | 2 +-
src/WinRestoreKit.Tests/BackupPresetsTests.cs | 10 +-
.../BackupResultTimelinePublicationTests.cs | 94 +
.../BackupWorkspaceViewModelTests.cs | 162 ++
.../BackupWorkspaceViewTests.cs | 35 +
.../ComparisonWorkspaceViewModelTests.cs | 222 ++
.../ConfirmViewModelTests.cs | 106 +
.../FileFolderStaleArtifactTests.cs | 104 +
.../LockedPayloadBackupTests.cs | 5 +-
src/WinRestoreKit.Tests/ModuleShapeTests.cs | 14 +-
.../ProgressPageViewTests.cs | 16 +-
.../ProgressWorkspaceViewModelTests.cs | 181 ++
.../RegFileValidateContentTests.cs | 56 +
.../RestoreConsentCancellationTests.cs | 6 +-
.../RestoreConsentDialogTests.cs | 42 +
.../RestoreContentsPayloadErrorTests.cs | 48 +
.../RestoreDialogOwnerTests.cs | 64 +
.../RestoreSetViewModelTests.cs | 36 +
.../ResultWorkspaceViewModelTests.cs | 93 +
src/WinRestoreKit.Tests/RunSummaryTests.cs | 24 +-
.../ScopeGroupsPrivacyTests.cs | 3 +-
.../ShellBackupFlowTests.cs | 96 +
.../SnapshotComparisonServiceTests.cs | 448 ++++
.../SnapshotEventCatalogTests.cs | 257 +++
.../SnapshotFolderPathTests.cs | 8 +-
.../SnapshotGateConsentTests.cs | 48 +
.../SnapshotPayloadPreparationServiceTests.cs | 132 ++
src/WinRestoreKit.Tests/SnapshotTests.cs | 16 +-
src/WinRestoreKit.Tests/ThemeServiceTests.cs | 49 +
src/WinRestoreKit.Tests/ThemeSettingsTests.cs | 31 +
.../TimelineAccessibilityTests.cs | 166 ++
.../TimelineViewModelTests.cs | 119 ++
.../TimelineWpfSmokeTests.cs | 99 +
.../UpdateCheckVerdictTests.cs | 51 +
.../VersionParsingTests.cs | 82 +-
.../ViewDataHelperTests.cs | 18 +-
.../WPowerPlansManifestPreClearTests.cs | 50 +
.../WinRestoreKit.Tests.csproj | 5 +
.../WpfAppRestoreDialogTests.cs | 46 +
src/WinRestoreKit.Tests/WpfLogSinkTests.cs | 42 +
src/WinRestoreKit.Tests/WpfRunUiTests.cs | 82 +
src/WinRestoreKit.Tests/WpfShellTests.cs | 124 ++
src/WinRestoreKit.Tests/WpfTestHost.cs | 90 +
src/WinRestoreKit.Wpf/App.xaml | 11 +
src/WinRestoreKit.Wpf/App.xaml.cs | 39 +
.../Infrastructure/AsyncDelegateCommand.cs | 45 +
.../Infrastructure/DelegateCommand.cs | 26 +
.../Infrastructure/ObservableObject.cs | 29 +
src/WinRestoreKit.Wpf/MainWindow.xaml | 86 +
src/WinRestoreKit.Wpf/MainWindow.xaml.cs | 24 +
.../Navigation/CompareWorkflowNavigator.cs | 82 +
.../Navigation/ITimelineNavigator.cs | 11 +
.../Properties/AssemblyInfo.cs | 3 +
.../Resources/SnapshotEventTemplates.xaml | 282 +++
.../Services/CompareDialogService.cs | 19 +
.../Services/ExternalLinkService.cs | 9 +
.../Services/ICompareDialogService.cs | 11 +
.../Services/IExternalLinkService.cs | 7 +
.../Services/IRunDialogService.cs | 12 +
.../Services/IRunPresentation.cs | 15 +
.../Services/ISystemThemeDetector.cs | 7 +
.../Services/IThemeService.cs | 13 +
.../Services/IWpfDialogService.cs | 10 +
.../Services/RestoreRunDialogService.cs | 31 +
.../Services/WindowsThemeDetector.cs | 27 +
.../Services/WpfAppRestoreDialog.cs | 47 +
.../Services/WpfDialogService.cs | 60 +
.../Services/WpfDispatcher.cs | 41 +
src/WinRestoreKit.Wpf/Services/WpfLogSink.cs | 49 +
src/WinRestoreKit.Wpf/Services/WpfRunUi.cs | 138 ++
.../Services/WpfThemeService.cs | 81 +
.../Services/WpfUpdatePresenter.cs | 62 +
src/WinRestoreKit.Wpf/Themes/Controls.xaml | 870 ++++++++
src/WinRestoreKit.Wpf/Themes/Dark.xaml | 84 +
src/WinRestoreKit.Wpf/Themes/Light.xaml | 84 +
.../ViewModels/AboutViewModel.cs | 34 +
.../ViewModels/AppRestoreDialogViewModel.cs | 182 ++
.../ViewModels/BackupRunCompletion.cs | 21 +
.../ViewModels/BackupRunRequest.cs | 24 +
.../ViewModels/BackupScopeItemViewModel.cs | 38 +
.../ViewModels/BackupWorkspaceViewModel.cs | 125 ++
.../ViewModels/ComparisonFilter.cs | 8 +
.../ComparisonWorkspaceViewModel.cs | 179 ++
.../ViewModels/ConfirmViewModel.cs | 173 ++
.../History/AdvancedHistoryViewModel.cs | 96 +
.../ModuleComparisonRowViewModel.cs | 65 +
.../ViewModels/ModuleImpactViewModel.cs | 27 +
.../ViewModels/ProgressWorkspaceViewModel.cs | 372 ++++
.../ViewModels/RestoreSetViewModel.cs | 47 +
.../ViewModels/ResultWorkspaceViewModel.cs | 55 +
.../ViewModels/SettingsViewModel.cs | 41 +
.../ViewModels/ShellViewModel.cs | 198 ++
.../Snapshots/SnapshotEventStatus.cs | 72 +
.../Snapshots/SnapshotEventViewModel.cs | 92 +
.../ViewModels/Timeline/TimelineViewModel.cs | 116 ++
src/WinRestoreKit.Wpf/Views/AboutView.xaml | 57 +
src/WinRestoreKit.Wpf/Views/AboutView.xaml.cs | 12 +
.../Views/AdvancedHistoryView.xaml | 158 ++
.../Views/AdvancedHistoryView.xaml.cs | 20 +
.../Views/AppRestoreDialog.xaml | 172 ++
.../Views/AppRestoreDialog.xaml.cs | 53 +
.../Views/BackupWorkspaceView.xaml | 343 ++++
.../Views/BackupWorkspaceView.xaml.cs | 25 +
.../Views/ComparisonWorkspaceView.xaml | 564 +++++
.../Views/ComparisonWorkspaceView.xaml.cs | 12 +
src/WinRestoreKit.Wpf/Views/ConfirmView.xaml | 342 ++++
.../Views/ConfirmView.xaml.cs | 26 +
.../Views/Controls/SnapshotEventList.xaml | 75 +
.../Views/Controls/SnapshotEventList.xaml.cs | 71 +
.../Views/Dialogs/RestoreConsentDialog.xaml | 86 +
.../Dialogs/RestoreConsentDialog.xaml.cs | 65 +
.../Views/ProgressWorkspaceView.xaml | 360 ++++
.../Views/ProgressWorkspaceView.xaml.cs | 12 +
.../Views/ResultWorkspaceView.xaml | 230 +++
.../Views/ResultWorkspaceView.xaml.cs | 12 +
src/WinRestoreKit.Wpf/Views/SettingsView.xaml | 69 +
.../Views/SettingsView.xaml.cs | 12 +
src/WinRestoreKit.Wpf/Views/TimelineView.xaml | 98 +
.../Views/TimelineView.xaml.cs | 22 +
.../WinRestoreKit.Wpf.csproj | 26 +
src/WinRestoreKit.sln | 44 +
src/WinRestoreKit/Forms/RestAppsForm.cs | 717 +------
src/WinRestoreKit/Helpers/UpdateCheck.cs | 138 --
.../Helpers/WinFormsUpdatePresenter.cs | 76 +
src/WinRestoreKit/MainForm.cs | 19 +-
src/WinRestoreKit/Program.cs | 71 +-
src/WinRestoreKit/Views/AboutPageView.cs | 20 +-
src/WinRestoreKit/Views/BackupPageView.cs | 117 +-
src/WinRestoreKit/Views/BackupPresets.cs | 39 -
src/WinRestoreKit/Views/ProgressPageView.cs | 4 +-
.../Views/RestoreWizardStep2View.cs | 12 +-
src/WinRestoreKit/WinRestoreKit.csproj | 1 +
191 files changed, 20727 insertions(+), 2013 deletions(-)
create mode 100644 assets/WinRestoreKit-one-pager.html
create mode 100644 docs/superpowers/plans/2026-08-09-backup-progress-results.md
create mode 100644 docs/superpowers/plans/2026-08-09-compare-confirm-restore.md
create mode 100644 docs/superpowers/plans/2026-08-09-timeline-compare-wpf-migration-roadmap.md
create mode 100644 docs/superpowers/plans/2026-08-09-timeline-event-model.md
create mode 100644 docs/superpowers/plans/2026-08-09-wpf-cutover-release-verification.md
create mode 100644 docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md
create mode 100644 docs/superpowers/specs/2026-08-09-timeline-compare-wpf-shell-design.md
create mode 100644 src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs
create mode 100644 src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs
create mode 100644 src/WinRestoreKit.Application/Backup/BackupPresets.cs
rename src/{WinRestoreKit/Views => WinRestoreKit.Application/Backup}/ScopeGroups.cs (75%)
create mode 100644 src/WinRestoreKit.Application/Comparison/ComparisonState.cs
create mode 100644 src/WinRestoreKit.Application/Comparison/ModuleComparison.cs
create mode 100644 src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs
create mode 100644 src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs
create mode 100644 src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs
rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/BackupRestoreOrchestrator.cs (98%)
rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/IRunUi.cs (74%)
rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/RunControl.cs (100%)
rename src/{WinRestoreKit => WinRestoreKit.Application}/Orchestration/RunCoordinator.cs (100%)
create mode 100644 src/WinRestoreKit.Application/Properties/AssemblyInfo.cs
rename src/{WinRestoreKit => WinRestoreKit.Application}/Results/RunSummary.cs (94%)
rename src/{WinRestoreKit/Helpers => WinRestoreKit.Application/Settings}/BackupRootRegistry.cs (100%)
create mode 100644 src/WinRestoreKit.Application/Settings/IThemeSettings.cs
create mode 100644 src/WinRestoreKit.Application/Settings/RegistryThemeSettings.cs
create mode 100644 src/WinRestoreKit.Application/Settings/ThemeMode.cs
rename src/{WinRestoreKit/Views => WinRestoreKit.Application/Snapshots}/BackupFolders.cs (55%)
create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEvent.cs
create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEventCatalog.cs
create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotEventKind.cs
create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparation.cs
create mode 100644 src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparationService.cs
create mode 100644 src/WinRestoreKit.Application/Updates/IUpdateCheckService.cs
create mode 100644 src/WinRestoreKit.Application/Updates/UpdateCheckResult.cs
create mode 100644 src/WinRestoreKit.Application/Updates/UpdateCheckService.cs
create mode 100644 src/WinRestoreKit.Application/Updates/UpdateVerdict.cs
create mode 100644 src/WinRestoreKit.Application/Updates/VersionInfo.cs
create mode 100644 src/WinRestoreKit.Application/WinRestoreKit.Application.csproj
create mode 100644 src/WinRestoreKit.Tests/AdvancedHistoryViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/AppRestoreServiceTests.cs
create mode 100644 src/WinRestoreKit.Tests/ApplicationBoundaryTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupModuleCatalogTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupOutputPathTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupResultTimelinePublicationTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/BackupWorkspaceViewTests.cs
create mode 100644 src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/ConfirmViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/FileFolderStaleArtifactTests.cs
create mode 100644 src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/RegFileValidateContentTests.cs
create mode 100644 src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs
create mode 100644 src/WinRestoreKit.Tests/RestoreContentsPayloadErrorTests.cs
create mode 100644 src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs
create mode 100644 src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/ShellBackupFlowTests.cs
create mode 100644 src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs
create mode 100644 src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs
create mode 100644 src/WinRestoreKit.Tests/SnapshotGateConsentTests.cs
create mode 100644 src/WinRestoreKit.Tests/SnapshotPayloadPreparationServiceTests.cs
create mode 100644 src/WinRestoreKit.Tests/ThemeServiceTests.cs
create mode 100644 src/WinRestoreKit.Tests/ThemeSettingsTests.cs
create mode 100644 src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs
create mode 100644 src/WinRestoreKit.Tests/TimelineViewModelTests.cs
create mode 100644 src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs
create mode 100644 src/WinRestoreKit.Tests/UpdateCheckVerdictTests.cs
create mode 100644 src/WinRestoreKit.Tests/WPowerPlansManifestPreClearTests.cs
create mode 100644 src/WinRestoreKit.Tests/WpfAppRestoreDialogTests.cs
create mode 100644 src/WinRestoreKit.Tests/WpfLogSinkTests.cs
create mode 100644 src/WinRestoreKit.Tests/WpfRunUiTests.cs
create mode 100644 src/WinRestoreKit.Tests/WpfShellTests.cs
create mode 100644 src/WinRestoreKit.Tests/WpfTestHost.cs
create mode 100644 src/WinRestoreKit.Wpf/App.xaml
create mode 100644 src/WinRestoreKit.Wpf/App.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs
create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/DelegateCommand.cs
create mode 100644 src/WinRestoreKit.Wpf/Infrastructure/ObservableObject.cs
create mode 100644 src/WinRestoreKit.Wpf/MainWindow.xaml
create mode 100644 src/WinRestoreKit.Wpf/MainWindow.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs
create mode 100644 src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs
create mode 100644 src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs
create mode 100644 src/WinRestoreKit.Wpf/Resources/SnapshotEventTemplates.xaml
create mode 100644 src/WinRestoreKit.Wpf/Services/CompareDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/ExternalLinkService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/ICompareDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/IExternalLinkService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/IRunDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/IRunPresentation.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/ISystemThemeDetector.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/IThemeService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/IWpfDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/RestoreRunDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WindowsThemeDetector.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfDialogService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfDispatcher.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfLogSink.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfRunUi.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfThemeService.cs
create mode 100644 src/WinRestoreKit.Wpf/Services/WpfUpdatePresenter.cs
create mode 100644 src/WinRestoreKit.Wpf/Themes/Controls.xaml
create mode 100644 src/WinRestoreKit.Wpf/Themes/Dark.xaml
create mode 100644 src/WinRestoreKit.Wpf/Themes/Light.xaml
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/AboutViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupRunCompletion.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupRunRequest.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupScopeItemViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ComparisonFilter.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ComparisonWorkspaceViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ConfirmViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/History/AdvancedHistoryViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ModuleComparisonRowViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ModuleImpactViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/RestoreSetViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/SettingsViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventStatus.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/ViewModels/Timeline/TimelineViewModel.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/AboutView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/AboutView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/ConfirmView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/ConfirmView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/SettingsView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/SettingsView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/Views/TimelineView.xaml
create mode 100644 src/WinRestoreKit.Wpf/Views/TimelineView.xaml.cs
create mode 100644 src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj
delete mode 100644 src/WinRestoreKit/Helpers/UpdateCheck.cs
create mode 100644 src/WinRestoreKit/Helpers/WinFormsUpdatePresenter.cs
delete mode 100644 src/WinRestoreKit/Views/BackupPresets.cs
diff --git a/assets/WinRestoreKit-one-pager.html b/assets/WinRestoreKit-one-pager.html
new file mode 100644
index 0000000..3e5ddcf
--- /dev/null
+++ b/assets/WinRestoreKit-one-pager.html
@@ -0,0 +1,567 @@
+
+
+
+
+
+ WinRestoreKit — Your Windows setup, saved locally
+
+
+
+
+
+
+
+
+
Back up the details that make Windows yours
+
Your Windows setup, saved locally.
+
+ WinRestoreKit backs up the settings that make a PC feel familiar—then helps restore
+ only the modules you choose. No cloud account. No telemetry. No .NET install.
+
+
+
+
+
+
+
+
+
+
+
+
Local by design
+
Your files stay with you.
+
Each timestamped local backup includes a machine-readable manifest and log, plus copied files and registry exports in a folder or compressed payload.
+
+
+
+
+
+
+
A restore path you can understand
+ backup_2026-08-09_1430
+
+
+
Choose Pick a preset or select individual modules.
+
Capture Create a lightweight, timestamped local backup.
+
Review See what restore will overwrite before you consent.
+
Restore Protect the current state with a pre-restore snapshot.
+
+
+
+
+
+
Back up more than a wallpaper
+
+
+
01
+
Windows personality Taskbar, themes, visual effects, regional preferences, accessibility settings, and more.
+
+
+
02
+
Devices and connections Printers, mouse and keyboard settings, Wi-Fi credentials, mapped drives, and network configuration.
+
+
+
03
+
Apps and developer tools Selected app settings, terminal and VS Code data, SSH configuration, hosts file, and a winget package list.
+
+
+
04
+
Evidence with every run Human-readable logs plus backup_manifest.json record what succeeded, skipped, or failed.
+
+
+
+ Selective modules Presets History timeline
+ Power plans Local folders Single executable
+
+
+
+
+
+
Safety without spin
+
Know what restore can—and cannot—undo.
+
Before changes begin, WinRestoreKit states what will be overwritten, asks for consent, and creates a pre-restore snapshot. Registry imports and folder copies are additive, so the app says plainly when restoring a snapshot cannot remove newly added values or files.
+
+
+
Backups stay local No cloud account or telemetry
+
1 executable Self-contained Windows download
+
Pick the scope Preset or module-by-module
+
Undo point Automatic snapshot before restore
+
+
+
+
+
+
+
Keep your Windows setup in your hands.
+
Download the latest release or inspect the source on GitHub.
+
+
+ Download WinRestoreKit.exe →
+
+
+ Requires Windows 11 x64 and administrator privileges. Windows 10 may work but is untested. The current binary is not code-signed, so SmartScreen may show a first-run warning. Source and licence: github.com/nicolasestrem/WinRestoreKit
+
+
+
+
+
diff --git a/docs/superpowers/plans/2026-08-09-backup-progress-results.md b/docs/superpowers/plans/2026-08-09-backup-progress-results.md
new file mode 100644
index 0000000..c74eadf
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-backup-progress-results.md
@@ -0,0 +1,1321 @@
+# Backup, Progress, Results, and App Restore WPF Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Deliver the WPF Create Snapshot workflow from module selection through safe execution, honest results, and return to an updated Timeline, including the owner-bound app-reinstall dialog, while the WinForms application remains runnable.
+
+**Architecture:** Reuse the Foundation plan's framework-neutral `WinRestoreKit.Application` runner, run-control, and summary contracts plus the Compare/Confirm plan's module-catalog contract without modifying Core backup/restore semantics or snapshot files. WPF view models own selection, navigation, presentation, and commands; Foundation's `WpfRunUi` and `WpfLogSink` are extended and composed with the dispatcher and owner-bound dialogs. A small Application completion publisher asks the Timeline event catalog to discover retained output first and creates an in-memory failure event only when no retained event exists.
+
+**Tech Stack:** .NET 8 for Windows, WPF/XAML, MVVM (`INotifyPropertyChanged` and `ICommand`), xUnit 2.9.3, existing Core `BackupBase`/payload/manifest/logging APIs, and the existing Windows registry-backed destination registry.
+
+## Global Constraints
+
+- Execute after `2026-08-09-wpf-foundation-application-shell.md`, `2026-08-09-timeline-event-model.md`, and `2026-08-09-compare-confirm-restore.md`; this plan does not duplicate their contracts.
+- Keep `WinRestoreKit.Core` backup, restore, manifest, payload, cleanup, containment, ownership, archive, rollback, Explorer, and shutdown semantics unchanged. Do not change the snapshot format.
+- `src/WinRestoreKit.Application` references Core only; it MUST NOT reference WPF or WinForms. WPF views and view models MUST NOT parse registry exports, JSON payload text, or archive contents.
+- Preserve the six ordered scopes, their current module membership, default-selection behavior, environment-variable explicit opt-in, warning text, `BackupPresets` literal membership, destination containment protection, name validation, and `SnapshotCompression.None/Fast/Max` behavior. The default compression remains `Fast`.
+- The only admission point is `RunCoordinator.TryStart()`. A rejected second attempt must not construct a runner, replace the active workspace, install a log sink, or mutate snapshot state. Release admission with `RunCoordinator.SetRunning(false)` in the run owner's `finally` path.
+- `RunControl.Pause()`, `Resume()`, and `RequestCancellation()` retain their existing boundary-only behavior: an active module finishes; no later module starts; cancellation wakes a paused run. Never claim rollback for work already performed.
+- Disable cancellation when the runner reports the exact `BackupRestoreOrchestrator.ArchiveProgressText` value. Preserve real metric values from `ProgressMetrics`; use `N/A` where Core reports no byte measurement, never an invented estimate.
+- `RunSummary` is the sole authority for completion wording. Render `RunSeverity { Information, Warning, Error }`; do not inspect `RunControl.IsCancellationRequested` to relabel a summary. In particular, a completed `RunSummary.For(...)` remains completed after a late cancel click, while `RunSummary.Incomplete(...)` remains visibly incomplete.
+- The Foundation `IRunUi` has no `IWin32Window Owner` and no Windows Forms type. `DialogOwner` is an opaque `object` supplied by the shell solely for Core's existing app-reinstall dialog seam; Application neither casts it nor passes it into `AppRestoreService`. Its exact surface is:
+
+ ```csharp
+ internal interface IRunUi
+ {
+ object DialogOwner { get; }
+ void SetProgressText(string text);
+ void SetProgressPercent(int percent);
+ void SetProgressDetail(string groupInfo, string elapsed, string remaining,
+ string throughput, long bytesWritten, int errors, int warnings);
+ void ShowSummary(RunSummary summary, string caption,
+ IReadOnlyList outcomes);
+ IReadOnlyList ShowConsentDialog(RestorePlan plan);
+ bool ConfirmSnapshotOverride(string text, string caption);
+ void ShowPlanCompositionError(string text, string caption);
+ void SetExplorerRestartVisible(bool visible);
+ }
+ ```
+
+- Use the Timeline plan's app-lifetime `SnapshotEventCatalog`: `IReadOnlyList Read()` and `void RecordSessionFailure(DateTime created, string displayName, string diagnosticReason)`. Its immutable events use `SnapshotEventKind { Verified, Partial, Failed, Unreadable }`; only `Verified` and `Partial` are restorable.
+- First re-read the catalog after a backup. Publish a session failure only when no retained recognized event corresponds to the expected backup path (for example, folder creation failed or cancellation removed the new folder). Retained partial, failed, or unreadable folders are discovered from disk and MUST NOT receive a duplicate session event. Never keep an incomplete folder merely to manufacture history.
+- The WPF shell registers Core's existing `AppStoreApps.RestoreDialog` with a callback that accepts only a live WPF `Window` owner; `AppRestoreService` has no owner argument. Source-selection/read or winget-install failures appear in the owner-bound WPF dialog or inline dialog state; never use an ownerless `MessageBox`.
+- Keep the original `src/WinRestoreKit` WinForms shell and `RestAppsForm` runnable during this phase. Port shared, non-UI app-restore logic out of the form and update the form to consume it; do not delete the form, designer, or legacy construction tests until Cutover.
+- Keep tests in `src/WinRestoreKit.Tests`. Preserve pure tests, update all `IRunUi` fakes for the Foundation interface, and use Foundation's `WpfTestHost.Run(...)` for WPF window/control construction. Do not run formatters, linters, builds, or tests while drafting this plan.
+
+## Prerequisite Interfaces
+
+Foundation supplies the Application runner/run-control/summary contracts, WPF shell/adapters, and `WpfTestHost`; Compare/Confirm supplies `BackupModuleCatalog`. Do not recreate any of them:
+
+```csharp
+// src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs (Compare/Confirm)
+public static IReadOnlyList BackupModuleCatalog.CreateAll();
+// BackupModuleRegistration exposes public BackupBase Module, string Category, string Title.
+
+// src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs
+internal BackupRestoreOrchestrator(IRunUi ui, RunControl runControl = null);
+internal Task RunBackup(IReadOnlyList modules, string destination,
+ string snapshotName, SnapshotCompression compression);
+internal Task RunRestore(IReadOnlyList modules, string backupPath);
+internal string BackupOutputPath { get; private set; }
+
+// src/WinRestoreKit.Application/Results/RunSummary.cs
+internal enum RunState { Problems, Done, NothingDone, Canceled, DidNotRun }
+internal enum RunSeverity { Information, Warning, Error }
+// RunSummary retains For(IReadOnlyList, bool, RunVerb, string),
+// Incomplete(IReadOnlyList, RunVerb, string), and Canceled(RunVerb).
+// It exposes State, Severity, Headline, and Detail, maps Problems (including Incomplete)
+// and DidNotRun to Warning, and all remaining current states to Information; it contains no MessageBoxIcon.
+
+// src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs
+public object CurrentWorkspace { get; private set; }
+public string WorkflowLabel { get; private set; }
+public ICommand ShowTimelineCommand { get; }
+internal void ShowTimeline();
+internal void NavigateTo(object workspace, string workflowLabel);
+
+// src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs
+void OpenCompare(SnapshotPayloadPreparation preparation);
+void ShowSnapshotDiagnostic(SnapshotEvent snapshot);
+```
+
+## File Map
+
+| Path | Responsibility |
+| --- | --- |
+| `src/WinRestoreKit.Application/Backup/ScopeGroups.cs` | Framework-neutral six-scope catalog and immutable scope rows. |
+| `src/WinRestoreKit.Application/Backup/BackupPresets.cs` | Literal Developer machine and Minimal privacy-safe preset memberships. |
+| `src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs` | Discover retained post-run event first; add session-only failed event only when discovery cannot represent the attempt. |
+| `src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs` | Shared app-export source discovery from current payload plus public Timeline events, payload preparation/disposal, parsing, list state, winget outcome wording, and install loop. |
+| `src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs` | Scope/preset/metadata selection and a validated, admitted backup request. |
+| `src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs` | Reusable non-reentrant `ICommand` that awaits view-model work and re-enables on completion. |
+| `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml` | Accessible Create Snapshot selection UI; no backup policy or payload parsing. |
+| `src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs` | One admitted run's live metrics, logs, pause/cancel commands, and terminal summary. |
+| `src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs` | Neutral summary/outcome rendering and return-to-Timeline command. |
+| `src/WinRestoreKit.Wpf/Services/WpfRunUi.cs` | Dispatcher-safe implementation of the neutral Application callback interface and owner-bound dialog bridge. |
+| `src/WinRestoreKit.Wpf/Services/WpfLogSink.cs` | Dispatcher-safe `ILogSink` that stops posting after disposal. |
+| `src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs` | Core `AppStoreApps.RestoreDialog` registration and owner-bound WPF package-picker construction. |
+| `src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml` and `.xaml.cs` | WPF package-picker dialog. |
+| `src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs` | App-export/list/install/stop state without WPF payload or JSON parsing. |
+| `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs`, `src/WinRestoreKit.Wpf/MainWindow.xaml` | Create Snapshot command, workspace data template, run availability, and return navigation. |
+| `src/WinRestoreKit/Views/BackupPageView.cs`, `src/WinRestoreKit/Forms/RestAppsForm.cs` | Still-runnable WinForms clients updated to consume moved shared selection/app-restore services; no duplicate logic. |
+| `src/WinRestoreKit.Tests/WpfTestHost.cs` | Foundation-owned deterministic STA/dispatcher helper reused by all WPF runtime tests. |
+| `src/WinRestoreKit.Tests/*Tests.cs` listed in each task | Focused regression, MVVM, dispatcher, dialog, and completion-publication coverage. |
+
+---
+
+### Task 1: Move shared backup-selection metadata and add completion publication
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Backup/ScopeGroups.cs`
+- Create: `src/WinRestoreKit.Application/Backup/BackupPresets.cs`
+- Create: `src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs`
+- Modify: `src/WinRestoreKit/Views/BackupPageView.cs`
+- Modify: `src/WinRestoreKit.Tests/ScopeGroupsPrivacyTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ViewDataHelperTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupPresetsTests.cs`
+- Create: `src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs`
+
+**Interfaces:**
+- Consumes: `BackupModuleCatalog.CreateAll()`, `SnapshotEventCatalog.Read()`, `SnapshotEvent.CanonicalPath`, and `RunSummary.Detail`; neither `BackupFolders` nor a WPF-local root parser is used.
+- Produces: `public sealed class ScopeGroupRow`; `public static class ScopeGroups` with `public static IReadOnlyList Build()`; `public static class BackupPresets` with `DeveloperMachine` and `MinimalPrivacySafeExclusions`; and `internal sealed class BackupCompletionPublisher` with `void Publish(string attemptedBackupPath, string displayName, RunSummary summary, DateTime created)`.
+
+- [ ] **Step 1: Write the failing scope, preset, and no-duplicate-event tests**
+
+ Move the existing pure scope/preset assertions away from `Views` and add completion-publication tests that pass the runner's exact attempted output path: create a recognized retained folder whose name deliberately differs from `Data.NowShort`, then use a fresh nonexistent attempted path for the session-only case.
+
+ ```csharp
+ [Fact]
+ public void Publish_RetainedPartialFolder_DoesNotAddSessionFailure()
+ {
+ using var isolation = new BackupRunIsolation();
+ string path = CreateRecognizedPartialFolder(isolation.DestinationRoot);
+ BackupRootRegistry.Remember(isolation.DestinationRoot);
+ var catalog = new SnapshotEventCatalog();
+ var publisher = new BackupCompletionPublisher(catalog);
+
+ publisher.Publish(path, "nightly", RunSummary.Incomplete(
+ Array.Empty(), RunVerb.Backup,
+ "Cancellation was requested. No further group was started."),
+ new DateTime(2026, 8, 9, 9, 0, 0));
+
+ SnapshotEvent discovered = Assert.Single(catalog.Read());
+ Assert.Equal(SnapshotEventKind.Partial, discovered.Kind);
+ Assert.Equal(Path.GetFullPath(path), discovered.CanonicalPath,
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static string CreateRecognizedPartialFolder(string root)
+ {
+ string path = Directory.CreateDirectory(Path.Combine(root, "snapshot-started-before-rollover")).FullName;
+ string manifest = BackupManifest.Compose(
+ new BackupBase[] { new DMouse() }, Array.Empty(),
+ new DateTime(2026, 8, 9, 9, 0, 0), "test-machine", "test-user", "test-build", "0.0.0");
+ File.WriteAllText(Path.Combine(path, BackupManifest.FileName), manifest);
+ return path;
+ }
+
+ [Fact]
+ public void Publish_NoRetainedRecognizedFolder_RecordsSessionFailureOnly()
+ {
+ var catalog = new SnapshotEventCatalog();
+ var publisher = new BackupCompletionPublisher(catalog);
+ var summary = RunSummary.For(Array.Empty(), false, RunVerb.Backup,
+ "the backup folder could not be created: access denied");
+
+ string attemptedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"),
+ "snapshot-attempted-before-rollover");
+ publisher.Publish(attemptedPath, "nightly", summary, new DateTime(2026, 8, 9, 9, 0, 0));
+
+ SnapshotEvent failure = Assert.Single(catalog.Read().Where(e => e.Kind == SnapshotEventKind.Failed));
+ Assert.Equal("nightly", failure.DisplayName);
+ Assert.Contains("could not be created", failure.DiagnosticReason, StringComparison.OrdinalIgnoreCase);
+ Assert.False(failure.IsRestorable);
+ }
+ ```
+
+ Keep the current literal expected scopes and type lists in `ViewDataHelperTests`; add assertions that `ScopeGroups.Build()` is directly importable from `WinRestoreKit`, not `Views`.
+
+- [ ] **Step 2: Run the focused tests to verify they fail before the move**
+
+ Run:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ScopeGroupsPrivacyTests|FullyQualifiedName~ViewDataHelperTests|FullyQualifiedName~BackupPresetsTests|FullyQualifiedName~BackupCompletionPublisherTests"
+ ```
+
+ Expected: FAIL because Application `ScopeGroups`, `BackupPresets`, and `BackupCompletionPublisher` do not exist yet. Do not accept a failure caused by a stale `Views` import as evidence that the new behavior is covered.
+
+- [ ] **Step 3: Move the selection data without changing its membership or defaults**
+
+ Move the implementation from `Views/ScopeGroups.cs` into the Application file, change its namespace to `WinRestoreKit`, make the row/catalog callable by WPF, and construct registrations through the Compare/Confirm `BackupModuleCatalog` facade. Preserve exactly the current six definitions, test-all-modules-once guard, detail truncation, and current default/opt-in calculation:
+
+ ```csharp
+ public sealed class ScopeGroupRow
+ {
+ internal ScopeGroupRow(string name, string detail, bool defaultChecked,
+ bool requiresExplicitOptIn, string cautionNote, IReadOnlyList modules)
+ {
+ Name = name;
+ Detail = detail;
+ DefaultChecked = defaultChecked;
+ RequiresExplicitOptIn = requiresExplicitOptIn;
+ CautionNote = cautionNote ?? string.Empty;
+ Modules = modules;
+ }
+
+ public string Name { get; }
+ public string Detail { get; }
+ public bool DefaultChecked { get; }
+ public bool RequiresExplicitOptIn { get; }
+ public string CautionNote { get; }
+ public IReadOnlyList Modules { get; }
+ }
+
+ public static class ScopeGroups
+ {
+ private const int DetailLimit = 96;
+ private static readonly ScopeDefinition[] Definitions =
+ {
+ new("Explorer & shell", IsExplorerAndShell),
+ new("Power & devices", IsPowerAndDevices),
+ new("Fonts", static module => module is WFonts),
+ new("Network profiles", IsNetworkProfile),
+ new("Environment variables (unfiltered)", static module => module is EEnvironment,
+ true, static modules => modules.Single().WarningMessage),
+ new("App settings (AppData)", IsAppSetting)
+ };
+
+ public static IReadOnlyList Build()
+ {
+ IReadOnlyList registrations = BackupModuleCatalog.CreateAll();
+ var modulesByScope = Definitions.ToDictionary(
+ definition => definition, _ => new List());
+ foreach (BackupModuleRegistration registration in registrations)
+ {
+ ScopeDefinition match = null;
+ foreach (ScopeDefinition definition in Definitions)
+ {
+ if (!definition.Includes(registration.Module))
+ continue;
+ if (match != null)
+ throw new InvalidOperationException(
+ $"Module '{registration.Module.GetType().FullName}' belongs to multiple backup scopes.");
+ match = definition;
+ }
+ if (match == null)
+ throw new InvalidOperationException(
+ $"Module '{registration.Module.GetType().FullName}' has no backup scope.");
+ modulesByScope[match].Add(registration.Module);
+ }
+ return Definitions.Select(definition =>
+ {
+ IReadOnlyList modules = modulesByScope[definition];
+ return new ScopeGroupRow(definition.Name,
+ modules.Count == 0 ? "No supported items detected" : Truncate(modules[0].Info),
+ !definition.RequiresExplicitOptIn && modules.Any(module => module.IsInstalled()),
+ definition.RequiresExplicitOptIn,
+ definition.CautionNoteFactory?.Invoke(modules) ?? string.Empty, modules);
+ }).ToList();
+ }
+
+ private static bool IsExplorerAndShell(BackupBase module) =>
+ module is WPersonalization or WVisualEffects or WTaskbar or WThemes or APinnedApps;
+ private static bool IsPowerAndDevices(BackupBase module) =>
+ module is WPowerPlans or DPrinters or DMouse or DKeyboard or DTouchpad;
+ private static bool IsNetworkProfile(BackupBase module) =>
+ module is WNetworkConf or WMappedDrives or CWiFiConf or EHosts;
+ private static bool IsAppSetting(BackupBase module) =>
+ module is WPrivacy or WAPrivacy or WTelemetry or WUpdates or WAccessibility or WRegional
+ or WOther or AppStoreApps or GGaming or ETerminal or EVSCode or ESsh
+ or EEnvironmentFiltered;
+ private static string Truncate(string value)
+ {
+ string singleLine = string.Join(" ", (value ?? string.Empty).Split(
+ (char[])null, StringSplitOptions.RemoveEmptyEntries));
+ return singleLine.Length <= DetailLimit ? singleLine :
+ singleLine.Substring(0, DetailLimit - 3).TrimEnd() + "...";
+ }
+
+ private sealed class ScopeDefinition
+ {
+ internal ScopeDefinition(string name, Func includes,
+ bool requiresExplicitOptIn = false,
+ Func, string> cautionNoteFactory = null)
+ {
+ Name = name; Includes = includes; RequiresExplicitOptIn = requiresExplicitOptIn;
+ CautionNoteFactory = cautionNoteFactory;
+ }
+ internal string Name { get; }
+ internal Func Includes { get; }
+ internal bool RequiresExplicitOptIn { get; }
+ internal Func, string> CautionNoteFactory { get; }
+ }
+ }
+ ```
+
+ `BackupPresets.cs` must keep these exact values and no UI dependency:
+
+ ```csharp
+ public static readonly IReadOnlyList DeveloperMachine =
+ new[] { "ETerminal", "EVSCode", "ESsh", "EEnvironment", "EHosts" };
+ public static readonly IReadOnlyList MinimalPrivacySafeExclusions =
+ new[] { "WUpdates", "EEnvironment", "EEnvironmentFiltered", "CWiFiConf" };
+ ```
+
+ Update `BackupPageView` to import these Application types and remove the old `Views` definitions; retain its form behavior so WinForms remains runnable. Do not leave an Application-to-WinForms forwarding shim.
+
+- [ ] **Step 4: Implement discovery-first completion publication**
+
+ Create a small Application-only publisher. It does not create files, write manifests, prune folders, or decide cleanup; those choices remain in `BackupRestoreOrchestrator`.
+
+ ```csharp
+ internal sealed class BackupCompletionPublisher
+ {
+ private readonly SnapshotEventCatalog catalog;
+
+ internal BackupCompletionPublisher(SnapshotEventCatalog catalog)
+ => this.catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
+
+ internal void Publish(string attemptedBackupPath, string displayName,
+ RunSummary summary, DateTime created)
+ {
+ string canonicalPath = TryCanonicalize(attemptedBackupPath);
+ bool retained = canonicalPath != null && catalog.Read().Any(snapshot =>
+ string.Equals(snapshot.CanonicalPath, canonicalPath,
+ StringComparison.OrdinalIgnoreCase));
+
+ if (retained)
+ return;
+
+ string diagnostic = summary?.Detail;
+ if (string.IsNullOrWhiteSpace(diagnostic))
+ diagnostic = "The snapshot run ended without a retained recognizable result.";
+
+ catalog.RecordSessionFailure(created, displayName ?? string.Empty, diagnostic);
+ }
+
+ private static string TryCanonicalize(string path)
+ {
+ try { return Path.GetFullPath(path); }
+ catch (Exception) { return null; }
+ }
+ }
+ ```
+
+ The publisher intentionally permits a session event even if a completed summary cannot be reconciled to a recognizable folder: the UI reports the observable discrepancy rather than inventing a verified snapshot. It intentionally does *not* publish where `Read()` finds the exact attempted canonical path as `Verified`, `Partial`, `Failed`, or `Unreadable`. It never computes a folder name or calls `Data.NowShort`; cleanup remains solely the runner's existing responsibility.
+
+- [ ] **Step 5: Run the focused tests to verify the shared contracts pass**
+
+ Run the Step 2 command again.
+
+ Expected: PASS. The six scopes retain their exact order and module assignment; unfiltered environment variables remain unchecked with their source warning; preset names still resolve; retained partial output yields exactly its discovered event; missing output yields one non-restorable current-session failure without filesystem retention.
+
+- [ ] **Step 6: Commit the shared selection and completion layer**
+
+ ```powershell
+ git add src/WinRestoreKit.Application/Backup/ScopeGroups.cs src/WinRestoreKit.Application/Backup/BackupPresets.cs src/WinRestoreKit.Application/Backup/BackupCompletionPublisher.cs src/WinRestoreKit/Views/BackupPageView.cs src/WinRestoreKit.Tests/ScopeGroupsPrivacyTests.cs src/WinRestoreKit.Tests/ViewDataHelperTests.cs src/WinRestoreKit.Tests/BackupPresetsTests.cs src/WinRestoreKit.Tests/BackupCompletionPublisherTests.cs
+ git commit -m "feat: share backup selection and completion events"
+ ```
+
+### Task 2: Build the accessible WPF Create Snapshot selection workspace
+
+**Files:**
+- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupRunRequest.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupScopeItemViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml.cs`
+- Modify: `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj`
+- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml`
+- Create: `src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs`
+- Create: `src/WinRestoreKit.Tests/BackupWorkspaceViewTests.cs`
+
+**Interfaces:**
+- Consumes: Task 1's `ScopeGroups.Build()` and `BackupPresets`; Foundation `ObservableObject`, `DelegateCommand`, and `RunCoordinator`; Compare/Confirm `BackupModuleCatalog`; `SnapshotCompression` from Core; and Foundation `WpfTestHost`.
+- Produces: `internal sealed class BackupRunRequest` (`IReadOnlyList Modules`, `string SnapshotName`, `SnapshotCompression Compression`, `string Destination`); `internal sealed class BackupWorkspaceViewModel`; and `internal Task StartAsync()` that invokes its supplied `Func` only after local selection/destination validation.
+
+- [ ] **Step 1: Write failing VM and STA view tests**
+
+ Reuse Foundation's `WpfTestHost.Run(Action)` for each test that constructs a WPF control or window. It supplies the STA thread, pumps its dispatcher, propagates action failures, and shuts the dispatcher down; do not create a second helper.
+
+ Test observable selection behavior, not XAML implementation details:
+
+ ```csharp
+ [Fact]
+ public async Task StartAsync_SelectedScope_RequestsItsConcreteModulesAndFastCompression()
+ {
+ BackupRunRequest request = null;
+ var vm = new BackupWorkspaceViewModel(r => { request = r; return Task.CompletedTask; }, @"C:\snapshots");
+ foreach (BackupScopeItemViewModel scope in vm.Scopes) scope.IsSelected = false;
+ vm.Scopes.Single(s => s.Name == "Explorer & shell").IsSelected = true;
+
+ await vm.StartAsync();
+
+ Assert.Equal(SnapshotCompression.Fast, request.Compression);
+ Assert.Equal(new[] { typeof(WPersonalization), typeof(WVisualEffects), typeof(WTaskbar),
+ typeof(WThemes), typeof(APinnedApps) }, request.Modules.Select(m => m.GetType()));
+ }
+
+ [Fact]
+ public async Task StartAsync_EmptySelectionOrDestination_ShowsValidationAndDoesNotRequestRun()
+ {
+ int starts = 0;
+ var vm = new BackupWorkspaceViewModel(_ => { starts++; return Task.CompletedTask; }, "");
+ foreach (BackupScopeItemViewModel scope in vm.Scopes) scope.IsSelected = false;
+
+ await vm.StartAsync();
+
+ Assert.Equal(0, starts);
+ Assert.Equal("Select at least one scope with supported items.", vm.ValidationMessage);
+ }
+
+ [Fact]
+ public void View_ExposesScopeWarningsAndLabeledCompressionChoices()
+ {
+ WpfTestHost.Run(() =>
+ {
+ var view = new BackupWorkspaceView { DataContext = new BackupWorkspaceViewModel(_ => Task.CompletedTask, @"C:\snapshots") };
+ Assert.NotNull(view.FindName("CreateSnapshotButton"));
+ Assert.NotNull(view.FindName("CompressionComboBox"));
+ });
+ }
+ ```
+
+ Add cases that pin: the unfiltered environment scope begins unselected and exposes `CautionNote`; blank destination produces the exact destination message once there is a scope; presets use the Task 1 literal lists; `None`, `Fast`, and `Max` map directly to their enum values; module flattening is `Distinct()` while preserving catalog order.
+
+- [ ] **Step 2: Run the new selection tests to verify they fail**
+
+ Run:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~BackupWorkspaceViewModelTests|FullyQualifiedName~BackupWorkspaceViewTests"
+ ```
+
+ Expected: FAIL at compile time because the WPF workspace and request do not exist. `WpfTestHost` is already supplied by Foundation and must not be recreated.
+
+- [ ] **Step 3: Implement the selection VM with no backup execution policy**
+
+ Keep request composition in the VM, but leave name safety, containment, destination creation, ownership, archiving, manifest writing, and custom-root persistence exclusively in `BackupRestoreOrchestrator`.
+
+ ```csharp
+ internal sealed class BackupWorkspaceViewModel : ObservableObject
+ {
+ private readonly Func startRunAsync;
+
+ internal BackupWorkspaceViewModel(Func startRunAsync, string defaultDestination)
+ {
+ this.startRunAsync = startRunAsync ?? throw new ArgumentNullException(nameof(startRunAsync));
+ Destination = defaultDestination ?? string.Empty;
+ Compression = SnapshotCompression.Fast;
+ Scopes = new ObservableCollection(
+ ScopeGroups.Build().Select(scope => new BackupScopeItemViewModel(scope)));
+ StartCommand = new AsyncDelegateCommand(StartAsync, ex =>
+ {
+ ValidationMessage = ex.Message;
+ OnPropertyChanged(nameof(ValidationMessage));
+ });
+ }
+
+ public ObservableCollection Scopes { get; }
+ public IReadOnlyList CompressionOptions { get; } =
+ new[] { SnapshotCompression.None, SnapshotCompression.Fast, SnapshotCompression.Max };
+ public string SnapshotName { get; set; } = string.Empty;
+ public string Destination { get; set; }
+ public SnapshotCompression Compression { get; set; }
+ public string ValidationMessage { get; private set; }
+ public ICommand StartCommand { get; }
+
+ internal async Task StartAsync()
+ {
+ IReadOnlyList modules = Scopes.Where(scope => scope.IsSelected)
+ .SelectMany(scope => scope.Modules).Distinct().ToArray();
+ if (modules.Count == 0)
+ {
+ ValidationMessage = "Select at least one scope with supported items.";
+ OnPropertyChanged(nameof(ValidationMessage));
+ return;
+ }
+ if (string.IsNullOrWhiteSpace(Destination))
+ {
+ ValidationMessage = "Choose a destination folder before capturing.";
+ OnPropertyChanged(nameof(ValidationMessage));
+ return;
+ }
+
+ ValidationMessage = null;
+ OnPropertyChanged(nameof(ValidationMessage));
+ await startRunAsync(new BackupRunRequest(modules, SnapshotName?.Trim() ?? string.Empty,
+ Compression, Destination.Trim()));
+ }
+ }
+ ```
+ If Foundation's `DelegateCommand` cannot await tasks, implement the following WPF-infrastructure command. Its `async void` method is the required `ICommand.Execute` boundary; it catches every task fault and is not a view click handler:
+
+ ```csharp
+ internal sealed class AsyncDelegateCommand : ICommand
+ {
+ private readonly Func executeAsync;
+ private readonly Action reportFailure;
+ private bool executing;
+
+ internal AsyncDelegateCommand(Func executeAsync, Action reportFailure = null)
+ {
+ this.executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
+ this.reportFailure = reportFailure;
+ }
+
+ public event EventHandler CanExecuteChanged;
+ public bool CanExecute(object parameter) => !executing;
+
+ public async void Execute(object parameter)
+ {
+ if (executing) return;
+ executing = true;
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ try { await executeAsync(); }
+ catch (Exception ex) { reportFailure?.Invoke(ex); }
+ finally
+ {
+ executing = false;
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+ ```
+
+ Keep this command internal to WPF infrastructure; do not use an `async void` view click handler.
+ `BackupScopeItemViewModel` carries `Name`, `Detail`, `CautionNote`, computed `HasCaution`, `RequiresExplicitOptIn`, `IReadOnlyList Modules`, and mutable `IsSelected` initialized from `DefaultChecked`. Implement commands for Select all, Clear, Developer machine, and Minimal privacy-safe by transforming the existing type-name lists through one tested `SelectModulesByTypeName`/exclusion routine; do not hard-code module type names in WPF.
+
+
+- [ ] **Step 4: Implement XAML for keyboard and automation parity**
+
+ Create a content view with the Foundation resource dictionaries and a named, labeled destination browser. Use `Microsoft.Win32.OpenFolderDialog` in code-behind only to set `Destination`; the binding and view model retain all selection state.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ Ensure every preset action has a text label, focus order follows scope list then metadata then action, and the caution is textual (not color-only). Add a `DataTemplate` for `BackupWorkspaceViewModel` to `MainWindow.xaml`; do not add a permanent navigation rail.
+
+- [ ] **Step 5: Run the selection tests to verify they pass**
+
+ Re-run the Step 2 command.
+
+ Expected: PASS. The view constructs on an STA thread, controls have automation names, scope choices match the existing module catalog, validation blocks an empty request, and no test invokes backup I/O.
+
+- [ ] **Step 6: Commit the WPF selection workspace**
+
+ ```powershell
+ git add src/WinRestoreKit.Wpf/Infrastructure/AsyncDelegateCommand.cs src/WinRestoreKit.Wpf/ViewModels/BackupRunRequest.cs src/WinRestoreKit.Wpf/ViewModels/BackupScopeItemViewModel.cs src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml src/WinRestoreKit.Wpf/Views/BackupWorkspaceView.xaml.cs src/WinRestoreKit.Wpf/MainWindow.xaml src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs src/WinRestoreKit.Tests/BackupWorkspaceViewTests.cs
+ git commit -m "feat: add WPF snapshot selection"
+ ```
+
+### Task 3: Add dispatcher-safe run progress, logging, and neutral result rendering
+
+**Files:**
+- Modify: `src/WinRestoreKit.Wpf/Services/WpfRunUi.cs`
+- Modify: `src/WinRestoreKit.Wpf/Services/WpfLogSink.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml.cs`
+- Modify: `src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs`
+- Modify: `src/WinRestoreKit.Tests/ArchiveProgressTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupDestinationContainmentTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs`
+- Modify: `src/WinRestoreKit.Tests/LockedPayloadBackupTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RestoreConsentCancellationTests.cs`
+- Modify: `src/WinRestoreKit.Tests/SnapshotFolderPathTests.cs`
+- Create: `src/WinRestoreKit.Tests/WpfRunUiTests.cs`
+- Create: `src/WinRestoreKit.Tests/WpfLogSinkTests.cs`
+- Create: `src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs`
+- Create: `src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs`
+- Create: `src/WinRestoreKit.Tests/BackupOutputPathTests.cs`
+
+**Interfaces:**
+
+- Consumes: Foundation `IRunUi`, `IRunPresentation`, `IRunDialogService`, `IWpfDialogService`, `WpfRunUi`, `WpfLogSink`, `BackupRestoreOrchestrator`, `RunControl`, `RunCoordinator`, `ProgressMetrics`, `RunSummary`, `RunSeverity`, Core `ILogSink`/`LogHelper`; Task 2 `BackupRunRequest`; and Compare/Confirm's owner-bound `RestoreRunDialogService`.
+- Produces: `BackupRestoreOrchestrator.BackupOutputPath` as the exact attempted output path; `internal sealed class ProgressWorkspaceViewModel : IRunPresentation` with `Task RunBackupAsync(BackupRunRequest request)`, `Task RunRestoreAsync(IReadOnlyList modules, string backupPath)`, `IReadOnlyList Outcomes`, and `string AttemptedBackupPath`; `internal sealed class ResultWorkspaceViewModel`; and extended dispatcher-bound Foundation `WpfRunUi`/`WpfLogSink`.
+
+- [ ] **Step 1: Write failing tests for admission-independent presentation, dispatch, cancellation, and results**
+
+ Update every existing orchestration fake to remove `IWin32Window Owner` and supply the opaque shell owner explicitly:
+
+ ```csharp
+ public object DialogOwner => null;
+ ```
+
+ Add focused WPF tests. The late-cancel test must render a completed summary *after* requesting cancellation and prove that the result remains complete:
+
+ ```csharp
+ [Fact]
+ public void Result_CompletedSummaryAfterLateCancel_IsNotRelabeledIncomplete()
+ {
+ var control = new RunControl();
+ control.RequestCancellation();
+ RunSummary completed = RunSummary.For(new[] { SucceededOutcome() }, true, RunVerb.Backup);
+
+ ResultWorkspaceViewModel vm = ResultWorkspaceViewModel.From(completed,
+ new[] { SucceededOutcome() }, () => Task.CompletedTask);
+
+ Assert.Equal("Run complete", vm.StatusLabel);
+ Assert.DoesNotContain("canceled", vm.Headline, StringComparison.OrdinalIgnoreCase);
+ Assert.Equal(RunSeverity.Information, vm.Severity);
+ }
+
+ [Fact]
+ public void Result_IncompleteSummary_UsesItsActualCancellationWording()
+ {
+ RunSummary incomplete = RunSummary.Incomplete(Array.Empty(), RunVerb.Backup,
+ "Cancellation was requested. No further group was started.");
+
+ ResultWorkspaceViewModel vm = ResultWorkspaceViewModel.From(incomplete,
+ Array.Empty(), () => Task.CompletedTask);
+
+ Assert.Equal("Run canceled, incomplete", vm.StatusLabel);
+ Assert.Contains("canceled, run incomplete", vm.Headline, StringComparison.OrdinalIgnoreCase);
+ Assert.Equal(RunSeverity.Warning, vm.Severity);
+ }
+
+ [Fact]
+ public void LogSink_AfterDispose_DoesNotPostAnotherLogLine()
+ {
+ WpfTestHost.Run(() =>
+ {
+ var lines = new List();
+ using var sink = new WpfLogSink(Dispatcher.CurrentDispatcher, lines.Add, () => lines.Clear());
+ sink.Append("before");
+ Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ sink.Dispose();
+ sink.Append("after");
+ Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ Assert.Equal(new[] { "before" }, lines);
+ });
+ }
+ ```
+
+ Add tests that `PauseCommand` changes only the requested boundary state and logs the exact pause/resume messages; confirmed cancellation disables pause/cancel and logs the exact active-group warning; archive progress disables cancel; a runner that returns without `ShowSummary` produces the existing did-not-run fallback; and `WpfRunUi.DialogOwner` returns the live `Window` when available and `null` when unavailable. The existing Compare/Confirm dialog-service tests remain the proof that consent and override calls fail closed without an owner.
+
+ Add `BackupOutputPathTests` for both existing backup overloads. The direct-path overload must set `BackupOutputPath` to the caller's `backupPath` before `RunBackupCore` validates/creates it. The destination overload must compute `backupPath = Path.Combine(destinationPath, Data.NowShort)`, assign that exact value to `BackupOutputPath` before `DestinationInsideSelectedSource`, and retain it even when validation produces a did-not-run summary. Use a deliberately pre-rollover-looking direct folder name in the test; assert equality to the supplied string, never a later `Data.NowShort` value.
+
+- [ ] **Step 2: Run the focused progress and adapter tests to verify they fail**
+
+ Run:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~WpfRunUiTests|FullyQualifiedName~WpfLogSinkTests|FullyQualifiedName~ProgressWorkspaceViewModelTests|FullyQualifiedName~ResultWorkspaceViewModelTests|FullyQualifiedName~ArchiveProgressTests|FullyQualifiedName~BackupDestinationContainmentTests|FullyQualifiedName~BackupDestinationLifecycleTests|FullyQualifiedName~BackupOutputPathTests|FullyQualifiedName~LockedPayloadBackupTests|FullyQualifiedName~RestoreConsentCancellationTests|FullyQualifiedName~SnapshotFolderPathTests"
+ ```
+
+ Expected: FAIL because `BackupOutputPath`, WPF progress/result workspaces, and the post-disposal log-sink behavior do not exist yet. The Foundation move has already replaced `Owner` with `DialogOwner`; a stale WinForms owner fake is a setup error, not evidence of this behavior.
+- [ ] **Step 3: Implement the dispatcher and dialog adapters**
+
+ Extend Foundation's existing `WpfLogSink` rather than replacing it: preserve its dispatcher-safe append/clear implementation and add the following disposed-state gate so no queued or later engine log line mutates the view after a run has ended:
+
+ ```csharp
+ internal sealed class WpfLogSink : ILogSink, IDisposable
+ {
+ private readonly Dispatcher dispatcher;
+ private readonly Action append;
+ private readonly Action clear;
+ private int disposed;
+
+ internal WpfLogSink(Dispatcher dispatcher, Action append, Action clear)
+ {
+ this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+ this.append = append ?? throw new ArgumentNullException(nameof(append));
+ this.clear = clear ?? throw new ArgumentNullException(nameof(clear));
+ }
+
+ public void Append(string text) => Post(() => append(text ?? string.Empty));
+ public void Clear() => Post(clear);
+ public void Dispose() => Interlocked.Exchange(ref disposed, 1);
+
+ private void Post(Action action)
+ {
+ if (Volatile.Read(ref disposed) != 0 || dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished)
+ return;
+ try { dispatcher.BeginInvoke(() => { if (Volatile.Read(ref disposed) == 0) action(); }); }
+ catch (InvalidOperationException) { }
+ }
+ }
+ ```
+
+ `WpfRunUi` is extended with its Foundation constructor shape—`Dispatcher`, `IRunPresentation`, `IRunDialogService`, and `Func ownerProvider`—not a second adapter API. `ProgressWorkspaceViewModel` is the concrete `IRunPresentation`: its setters update bound values and `ShowSummary` stores the exact supplied `RunSummary`/outcomes before returning, so navigation cannot race a blank result page. `WpfRunUi.DialogOwner` returns the visible, non-closing `Window` or `null`; it does not call the app-reinstall UI itself. Fire-and-forget presentation setters use `BeginInvoke`; callbacks requiring a result and `ShowSummary` use `Dispatcher.Invoke`. It forwards dialog calls only through the supplied `IRunDialogService`, whose concrete shell implementation owns fail-closed modal behavior.
+ The owner-bound `IRunDialogService` is Compare/Confirm's `RestoreRunDialogService`; do not create another dialog interface, owner protocol, consent dialog, or snapshot-override implementation here. This task only consumes the already composed service when `RunRestoreAsync` is invoked; Compare/Confirm owns its owner validation and fail-closed behavior.
+
+ Render `RunSeverity` in the WPF service/resource layer; no `MessageBoxIcon` appears in Application, WPF VM, or XAML.
+
+- [ ] **Step 4: Implement the run VM and XAML views**
+
+ Create exactly one `RunControl` and `BackupRestoreOrchestrator` per progress VM. The caller owns admission: this VM never calls `TryStart`, `SetRunning(true)`, or `SetRunning(false)`. It always copies `runner.BackupOutputPath` to `AttemptedBackupPath`, clears the global log sink in `finally`, and returns the terminal summary to its caller.
+
+
+ ```csharp
+ internal string BackupOutputPath { get; private set; }
+
+ internal Task RunBackup(IReadOnlyList selection, string backupPath)
+ {
+ BackupOutputPath = backupPath;
+ return RunBackupCore(selection, backupPath, null, SnapshotCompression, null);
+ }
+
+ internal Task RunBackup(IReadOnlyList selection, string destinationPath,
+ string snapshotName, SnapshotCompression compression)
+ {
+ if (!BackupNaming.TryValidateCustomName(snapshotName, out string safeSnapshotName))
+ {
+ ui.ShowSummary(RunSummary.For(new List(), false, RunVerb.Backup,
+ "the snapshot name is not a safe single folder name"), "Backup",
+ new List());
+ return Task.CompletedTask;
+ }
+ if (!IsKnownCompression(compression))
+ {
+ ui.ShowSummary(RunSummary.For(new List(), false, RunVerb.Backup,
+ "the selected compression mode is not supported"), "Backup",
+ new List());
+ return Task.CompletedTask;
+ }
+ if (string.IsNullOrWhiteSpace(destinationPath))
+ {
+ ui.ShowSummary(RunSummary.For(new List(), false, RunVerb.Backup,
+ "the backup destination is empty"), "Backup", new List());
+ return Task.CompletedTask;
+ }
+
+ SnapshotCompression = compression;
+ string backupPath = Path.Combine(destinationPath, Data.NowShort);
+ BackupOutputPath = backupPath;
+ if (DestinationInsideSelectedSource(backupPath, selection, out string containingSource))
+ {
+ ui.ShowSummary(RunSummary.For(new List(), false, RunVerb.Backup,
+ "the chosen destination is inside a folder this backup would copy (" + containingSource
+ + "), which would copy the backup into itself; choose a destination outside the "
+ + "folders being backed up"), "Backup", new List());
+ return Task.CompletedTask;
+ }
+ return RunBackupCore(selection, backupPath, safeSnapshotName, compression, destinationPath);
+ }
+ ```
+
+ Construct each `ProgressWorkspaceViewModel` with the live WPF `Dispatcher`, `Func ownerProvider`, Compare/Confirm's `IRunDialogService`, and Foundation `IWpfDialogService`. When it starts an already admitted run, it creates exactly one `RunControl`, `WpfRunUi(dispatcher, this, runDialogService, ownerProvider)`, `BackupRestoreOrchestrator`, and `WpfLogSink`; it never calls `RunCoordinator.TryStart`, `SetRunning`, or navigates the shell. Its owner passes that factory/dispatcher in production and a deterministic test factory in VM tests.
+
+ ```csharp
+ internal async Task RunBackupAsync(BackupRunRequest request)
+ {
+ LogHelper.Instance.SetSink(logSink);
+ sinkInstalled = true;
+ SetProgressText("Started snapshot " + request.SnapshotName + ".");
+ try
+ {
+ await runner.RunBackup(request.Modules, request.Destination,
+ request.SnapshotName, request.Compression);
+ if (summary == null)
+ SetSummary(RunSummary.For(Array.Empty(), false, RunVerb.Backup,
+ "the backup runner returned without a result"), Array.Empty());
+ return summary;
+ }
+ catch (Exception ex)
+ {
+ SetSummary(RunSummary.For(Array.Empty(), false, RunVerb.Backup, ex.Message),
+ Array.Empty());
+ return summary;
+ }
+ finally
+ {
+ AttemptedBackupPath = runner.BackupOutputPath;
+ if (sinkInstalled) LogHelper.Instance.SetSink(null);
+ sinkInstalled = false;
+ logSink.Dispose();
+ }
+ }
+ ```
+
+ The restore overload follows the same lifecycle with `runner.RunRestore`; it reuses `WpfRunUi` for the Compare/Confirm plan and must not create its own restore gates. `PauseCommand` toggles only `RunControl.Pause/Resume`; its log text is exactly `Run paused. The active group will finish before pausing.` or `Run resumed. The next group may start.`. `CancelCommand` asks through Foundation's already owner-bound `IWpfDialogService.Confirm` with the existing accurate warning, then calls `RequestCancellation`, disables pause/cancel, and appends `Cancellation requested. The active group will finish before cancellation.`.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+ `ResultWorkspaceViewModel.From` derives `StatusLabel`, `Headline`, `Detail`, `Severity`, and outcome rows solely from the passed summary/outcomes. It accepts a `Func` return action and exposes it through an `AsyncDelegateCommand`. Its only special labels are exact facts already encoded in the summary: `Run canceled, no changes` for `RunState.Canceled`; `Run canceled, incomplete` for the `RunSummary.Incomplete` headline; otherwise `Run complete`. It never reads `RunControl`.
+
+ Bind progress to text, percent, elapsed/remaining, throughput, bytes, error/warning counts, an `ObservableCollection`, and Pause/Cancel commands. Bind results to the neutral severity icon/text, headline/detail, module outcome list, and a text-labeled `Back to Timeline` command. Give every actionable control and live log panel an AutomationProperties name. Do not replace the workspace with a giant failure banner.
+
+- [ ] **Step 5: Run the focused tests to verify the adapters and result behavior pass**
+
+ Re-run the Step 2 command.
+
+ Expected: PASS. Orchestrator regression tests compile against the `IRunUi` opaque `DialogOwner` contract with no Windows Forms type; dispatcher callbacks do not update after disposal; pause/cancel retain boundary-only wording; archive disables cancellation; summary severity/writing is neutral; a late cancellation cannot rewrite a completed outcome.
+
+- [ ] **Step 6: Commit progress, logging, and result rendering**
+
+ ```powershell
+ git add src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs src/WinRestoreKit.Wpf/Services/WpfRunUi.cs src/WinRestoreKit.Wpf/Services/WpfLogSink.cs src/WinRestoreKit.Wpf/ViewModels/ProgressWorkspaceViewModel.cs src/WinRestoreKit.Wpf/ViewModels/ResultWorkspaceViewModel.cs src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml src/WinRestoreKit.Wpf/Views/ProgressWorkspaceView.xaml.cs src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml src/WinRestoreKit.Wpf/Views/ResultWorkspaceView.xaml.cs src/WinRestoreKit.Tests/ArchiveProgressTests.cs src/WinRestoreKit.Tests/BackupDestinationContainmentTests.cs src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs src/WinRestoreKit.Tests/BackupOutputPathTests.cs src/WinRestoreKit.Tests/LockedPayloadBackupTests.cs src/WinRestoreKit.Tests/RestoreConsentCancellationTests.cs src/WinRestoreKit.Tests/SnapshotFolderPathTests.cs src/WinRestoreKit.Tests/WpfRunUiTests.cs src/WinRestoreKit.Tests/WpfLogSinkTests.cs src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs
+ git commit -m "feat: add WPF run progress and results"
+ ```
+
+### Task 4: Port the app-reinstall dialog behind the WPF owner boundary
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml.cs`
+- Modify: `src/WinRestoreKit/Forms/RestAppsForm.cs`
+- Modify: `src/WinRestoreKit.Tests/AppRestoreDialogTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ModuleShapeTests.cs`
+- Create: `src/WinRestoreKit.Tests/WpfAppRestoreDialogTests.cs`
+
+**Interfaces:**
+- Consumes: Foundation's opaque `IRunUi.DialogOwner`; Core `AppStoreApps.RestoreDialog`, `AppStoreApps.ExportPathIn`, `BackupPayload.TryPrepareForRead`, `BackupPayload.ReadScope`, `Utils.RunWingetAsync`, and `ProcessOutcome`; Timeline `SnapshotEventCatalog.Read()` and public `SnapshotEvent`.
+- Produces: `internal enum AppExportState { Ok, Absent, Unreadable }`; immutable internal `AppRestoreSource`, `AppExport`, `AppRestoreListState`, and `AppRestoreOutcome`; `internal static class AppRestoreService` with `BuildSources(string selectedRestorePath, IReadOnlyList snapshots)`, `ReadFromSource(string sourcePath)`, `ComposeListState(AppExport export)`, and `InstallAsync(IReadOnlyList packageIdentifiers, Func stopRequested)`; and an internal WPF registration that assigns Core's existing `AppStoreApps.RestoreDialog`.
+
+- [ ] **Step 1: Write failing tests for the shared app-restore contract and WPF ownership**
+
+ Move the current pure tests for source order/deduplication, absent versus unreadable export, package parsing, list enablement, winget outcome description, and stopped/failed wording from `RestAppsForm` nested types to `AppRestoreService`. Keep the current assertions that an empty package list is `Ok`, a missing `Packages` array is unreadable, blank package identifiers are omitted, and a `ProcessOutcome.OutcomeUnknown` is never described as never started. Add a catalog-source test: the selected payload remains first, a later `Verified`/`Partial` event is added in catalog order, and `Failed`/`Unreadable` events or duplicate canonical paths do not become app-restore sources.
+
+ Test the WPF Core callback without a modal window by injecting its show action:
+
+ ```csharp
+ [Fact]
+ public void RestoreDialogCallback_PassesTheWpfWindowAndPreparedPath()
+ {
+ WpfTestHost.Run(() =>
+ {
+ var owner = new Window();
+ owner.Show();
+ Window receivedOwner = null;
+ string receivedPath = null;
+ Action callback = WpfAppRestoreDialog.CreateCallback(
+ (actualOwner, path) => { receivedOwner = actualOwner; receivedPath = path; });
+
+ try
+ {
+ callback(@"C:\prepared", owner);
+ Assert.Same(owner, receivedOwner);
+ Assert.Equal(@"C:\prepared", receivedPath);
+ }
+ finally
+ {
+ owner.Close();
+ }
+ });
+ }
+
+ [Fact]
+ public void RestoreDialogCallback_WithoutWpfOwner_DoesNotOpenDialog()
+ {
+ Action callback = WpfAppRestoreDialog.CreateCallback((_, _) =>
+ throw new Xunit.Sdk.XunitException("must not open"));
+ callback(@"C:\prepared", new object());
+ }
+ ```
+ Also assert that a newly constructed but hidden WPF `Window` does not invoke the show action; only a loaded, visible WPF owner may open the modal dialog.
+
+ Add a compressed-source test proving `ReadFromSource` disposes its private `BackupPayload.ReadScope` after copying package identifiers into the immutable result. Add an internal-service test whose fake installer requests stop after one package and expects `Stopped after 1 of 2 app(s). The remaining 1 were not started.`; do not kill an in-flight winget process.
+
+- [ ] **Step 2: Run the app-restore tests to verify they fail**
+
+ Run:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~AppRestoreDialogTests|FullyQualifiedName~RestoreDialogOwnerTests|FullyQualifiedName~ModuleShapeTests|FullyQualifiedName~WpfAppRestoreDialogTests"
+ ```
+
+ Expected: FAIL because the Application app-restore service and WPF owner port do not exist; existing tests still name the WinForms nested helper types.
+
+- [ ] **Step 3: Move all non-UI app-restore behavior into Application**
+
+ Create immutable WPF-safe data contracts and move the current `RestAppsForm` parsing, source-deduplication, list-enable, problem-routing, winget-description, and outcome-composition logic into this one Application service. Its internal entry points expose no `ProcessOutcome` or payload scope; the one installer overload that accepts a process delegate remains internal solely for the Application friend test assembly to cover all outcome branches deterministically:
+
+ ```csharp
+ internal enum AppExportState { Ok, Absent, Unreadable }
+
+ internal enum AppRestoreProblemRouting { None, ShowNow, Defer }
+
+ internal sealed class AppRestoreSource
+ {
+ internal AppRestoreSource(string path, string displayName, bool isSelectedRestoreSource)
+ {
+ Path = path ?? string.Empty;
+ DisplayName = displayName ?? string.Empty;
+ IsSelectedRestoreSource = isSelectedRestoreSource;
+ }
+ public string Path { get; }
+ public string DisplayName { get; }
+ public bool IsSelectedRestoreSource { get; }
+ }
+
+ internal sealed class AppExport
+ {
+ private AppExport(AppExportState state, IReadOnlyList packageIdentifiers, string message)
+ {
+ State = state;
+ PackageIdentifiers = packageIdentifiers ?? Array.Empty();
+ Message = message ?? string.Empty;
+ }
+ internal AppExportState State { get; }
+ internal IReadOnlyList PackageIdentifiers { get; }
+ internal string Message { get; }
+ internal bool IsProblem => State == AppExportState.Unreadable;
+ internal static AppExport Ok(IReadOnlyList ids, string message) => new(AppExportState.Ok, ids, message);
+ internal static AppExport Absent(string message) => new(AppExportState.Absent, null, message);
+ internal static AppExport Unreadable(string message) => new(AppExportState.Unreadable, null, message);
+ }
+
+ internal sealed class AppRestoreListState
+ {
+ internal AppRestoreListState(IReadOnlyList items, bool installEnabled)
+ {
+ Items = items ?? Array.Empty();
+ InstallEnabled = installEnabled;
+ }
+ internal IReadOnlyList Items { get; }
+ internal bool InstallEnabled { get; }
+ }
+
+ internal sealed class AppRestoreOutcome
+ {
+ internal AppRestoreOutcome(string caption, string text, RunSeverity severity)
+ => (Caption, Text, Severity) = (caption ?? string.Empty, text ?? string.Empty, severity);
+ internal string Caption { get; }
+ internal string Text { get; }
+ internal RunSeverity Severity { get; }
+ }
+
+ internal static class AppRestoreService
+ {
+ internal static IReadOnlyList BuildSources(
+ string selectedRestorePath, IReadOnlyList snapshots)
+ {
+ var sources = new List();
+ AddDistinct(sources, selectedRestorePath, "Selected restore source", true);
+ foreach (SnapshotEvent snapshot in snapshots ?? Array.Empty())
+ {
+ if (!snapshot.IsRestorable)
+ continue;
+ AddDistinct(sources, snapshot.CanonicalPath, snapshot.DisplayName, false);
+ }
+ return sources;
+ }
+
+ private static void AddDistinct(List sources, string path,
+ string displayName, bool isSelectedRestoreSource)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return;
+ string canonicalPath;
+ try { canonicalPath = Path.GetFullPath(path); }
+ catch (Exception) { return; }
+ if (sources.Any(source => string.Equals(source.Path, canonicalPath,
+ StringComparison.OrdinalIgnoreCase)))
+ return;
+ sources.Add(new AppRestoreSource(canonicalPath, displayName, isSelectedRestoreSource));
+ }
+
+ internal static AppRestoreListState ComposeListState(AppExport export) =>
+ export == null
+ ? new AppRestoreListState(null, false)
+ : new AppRestoreListState(export.PackageIdentifiers,
+ export.PackageIdentifiers.Count > 0);
+
+ internal static AppRestoreProblemRouting RouteProblem(AppExport export, bool windowShown)
+ {
+ if (export == null || !export.IsProblem)
+ return AppRestoreProblemRouting.None;
+ return windowShown ? AppRestoreProblemRouting.ShowNow : AppRestoreProblemRouting.Defer;
+ }
+
+ internal static AppExport ReadFromSource(string sourcePath)
+ {
+ if (!BackupPayload.TryPrepareForRead(sourcePath, out BackupPayload.ReadScope scope, out string error))
+ return AppExport.Unreadable("Could not prepare the app export source: " + error);
+ using (scope)
+ return ReadExport(AppStoreApps.ExportPathIn(scope.Path));
+ }
+
+ internal static Task InstallAsync(
+ IReadOnlyList packageIdentifiers, Func stopRequested)
+ => InstallAsync(packageIdentifiers, id => Utils.RunWingetAsync(true, "install",
+ "--id", id, "--accept-source-agreements", "--accept-package-agreements"), stopRequested);
+
+ internal static async Task InstallAsync(
+ IReadOnlyList packageIdentifiers,
+ Func> installOneAsync, Func stopRequested)
+ {
+ string[] requested = (packageIdentifiers ?? Array.Empty())
+ .Where(id => !string.IsNullOrWhiteSpace(id)).ToArray();
+ var failures = new List();
+ int attempted = 0;
+
+ foreach (string id in requested)
+ {
+ if (stopRequested?.Invoke() == true)
+ break;
+ attempted++;
+ string reason = Describe(await installOneAsync(id));
+ if (reason != null)
+ failures.Add(id + ": " + reason);
+ }
+
+ return ComposeOutcome(requested.Length, attempted, failures);
+ }
+ }
+ ```
+ Move `RestAppsForm.AppExport.Read` and `Parse` verbatim in behavior into `ReadFromSource`: only `FileNotFoundException` maps to `Absent`; every other read failure maps to `Unreadable`; empty JSON, malformed JSON, or a missing `Sources[0].Packages` array maps to `Unreadable`; nonblank `PackageIdentifier` values stay in file order. `ComposeListState` enables installation only when there is at least one identifier. `Describe` and `ComposeOutcome` retain the existing exact wording, but `AppRestoreOutcome` carries `Caption`, `Text`, and `RunSeverity` instead of `MessageBoxIcon`: no selected apps, stopped-without-failures, and complete installs are `Information`; stopped-with-failures and completed failures are `Warning`.
+
+ Update `RestAppsForm` to consume this service for source list (passing `new SnapshotEventCatalog().Read()`), parser/list state, descriptions, and final outcome. Retain its WinForms presentation and user-close behavior; leave `Program.RegisterUiSeams()` as the WinForms-specific `AppStoreApps.RestoreDialog` registration that opens the form. Delete the form's nested business types and helper copies. Application orchestration remains UI-agnostic and forwards only `IRunUi.DialogOwner` to Core's existing `AppStoreApps.RestoreAsync`.
+
+- [ ] **Step 4: Implement the owner-bound WPF dialog**
+
+ Register the WPF shell callback once while composing the app-lifetime `SnapshotEventCatalog`, before any restore can be admitted. It retains Core's established `Action` seam, rejects non-WPF owners, and invokes its modal dialog on the owner dispatcher:
+
+ ```csharp
+ internal sealed class WpfAppRestoreDialog
+ {
+ private readonly SnapshotEventCatalog catalog;
+
+ internal WpfAppRestoreDialog(SnapshotEventCatalog catalog)
+ => this.catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
+
+ internal static void Register(SnapshotEventCatalog catalog)
+ {
+ var dialog = new WpfAppRestoreDialog(catalog);
+ AppStoreApps.RestoreDialog = CreateCallback(dialog.Show);
+ }
+
+ internal static Action CreateCallback(Action show)
+ {
+ return (path, owner) =>
+ {
+ if (owner is not Window window || !window.IsLoaded || !window.IsVisible ||
+ window.Dispatcher.HasShutdownStarted || window.Dispatcher.HasShutdownFinished)
+ return;
+ Action open = () => show(window, path);
+ if (window.Dispatcher.CheckAccess()) open();
+ else window.Dispatcher.Invoke(open);
+ };
+ }
+
+ internal void Show(Window owner, string payloadPath)
+ {
+ var dialog = new AppRestoreDialog(payloadPath, catalog.Read()) { Owner = owner };
+ dialog.ShowDialog();
+ }
+ }
+ ```
+
+ `AppRestoreDialog` has `ShowInTaskbar="False"` and `WindowStartupLocation="CenterOwner"`. Its VM calls `AppRestoreService.BuildSources(payloadPath, snapshots)` and `ReadFromSource`; it receives only `AppRestoreSource`/immutable parsed values and never reads JSON, archives, registry keys, or `BackupFolders`. The dialog shows selected restore source first, a labeled source chooser, checkbox package rows, `Install selected apps`, and a text-labeled cancel/stop action. During install it disables source/package/install controls. Stop sets the VM flag, changes the action to the exact `Stopping after the current app (or its timeout)`, and lets the Application service finish the active package before the next boundary. A user-requested close while installing requests the same stop and defers close until the loop completes; dispatcher shutdown or owner teardown is never vetoed. Owner-bound unreadable-export/final outcome dialogs use the visible app-restore window; if it is no longer visible, append the message to `LogHelper` instead.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- [ ] **Step 5: Run the app-restore tests to verify they pass**
+
+ Re-run the Step 2 command.
+
+ Expected: PASS. Existing parser/winget facts are preserved in Application, compressed source preparation disposes its temporary scope, only a WPF `Window` owner invokes the registered Core callback, the callback preserves that owner and prepared path, and stop wording remains honest about work not started.
+
+- [ ] **Step 6: Commit the app-restore dialog port**
+
+ ```powershell
+ git add src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs src/WinRestoreKit.Wpf/Services/WpfAppRestoreDialog.cs src/WinRestoreKit.Wpf/ViewModels/AppRestoreDialogViewModel.cs src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml src/WinRestoreKit.Wpf/Views/AppRestoreDialog.xaml.cs src/WinRestoreKit/Forms/RestAppsForm.cs src/WinRestoreKit.Tests/AppRestoreDialogTests.cs src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs src/WinRestoreKit.Tests/ModuleShapeTests.cs src/WinRestoreKit.Tests/WpfAppRestoreDialogTests.cs
+ git commit -m "feat: port app restore dialog to WPF"
+ ```
+
+### Task 5: Wire Create Snapshot through admission, results, event publication, and Timeline return
+
+**Files:**
+- Modify: `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs`
+- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml`
+- Modify: `src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/BackupRunCompletion.cs`
+- Create: `src/WinRestoreKit.Tests/ShellBackupFlowTests.cs`
+- Create: `src/WinRestoreKit.Tests/BackupResultTimelinePublicationTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RunCoordinatorTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ProgressPageViewTests.cs`
+
+**Interfaces:**
+- Consumes: Tasks 1–4; Foundation `ShellViewModel.NavigateTo`, `ShowTimeline`, `RunCoordinator`; Timeline `SnapshotEventCatalog` and `TimelineViewModel.RefreshAsync(CancellationToken)`; no new competing global navigation service.
+- Produces: `internal sealed class BackupRunCompletion` (`RunSummary Summary`, `IReadOnlyList Outcomes`, `string AttemptedBackupPath`); a testable `Func>` shell seam; and the end-to-end `Create snapshot → BackupWorkspaceViewModel → ProgressWorkspaceViewModel → ResultWorkspaceViewModel → refreshed Timeline` sequence.
+
+- [ ] **Step 1: Write failing host-flow and publication tests**
+
+ Test atomic admission and navigation without executing a real backup by supplying the shell's explicit run delegate:
+
+ ```csharp
+ [Fact]
+ public async Task CreateSnapshot_AdmitsOneRun_ShowsResult_ThenRefreshesTimeline()
+ {
+ using var isolation = new BackupRunIsolation();
+ BackupRootRegistry.Remember(isolation.DestinationRoot);
+ RunCoordinator.SetRunning(false);
+ int refreshes = 0;
+ RunSummary summary = RunSummary.For(new[] { SucceededOutcome() }, true, RunVerb.Backup);
+ var shell = ShellViewModel.ForTest(
+ _ => Task.FromResult(new BackupRunCompletion(summary, new[] { SucceededOutcome() },
+ @"C:\snapshots\snapshot-started-before-rollover")),
+ new SnapshotEventCatalog(),
+ () => { refreshes++; return Task.CompletedTask; });
+
+ shell.CreateSnapshotCommand.Execute(null);
+ var selection = Assert.IsType(shell.CurrentWorkspace);
+ await selection.StartAsync();
+
+ Assert.IsType(shell.CurrentWorkspace);
+ Assert.False(RunCoordinator.IsRunning);
+ Assert.Equal(1, refreshes);
+ ((ResultWorkspaceViewModel)shell.CurrentWorkspace).ReturnToTimelineCommand.Execute(null);
+ Assert.Equal(1, refreshes);
+ }
+ ```
+
+ Add a second concurrent-start test that pre-sets `RunCoordinator` true, invokes `StartAsync`, and proves the workspace remains selection, no log sink/run factory is used, and its visible message is `Another backup or restore is already running.`. Add a completion-publication test for a canceled-new-folder cleanup: after the runner's `Incomplete` summary and no retained recognized path, the Timeline read contains one current-session non-restorable `Failed` event; when the retained path is discovered as Partial, only the persisted Partial event exists.
+
+ Retain the old `ProgressPageViewTests` while WinForms is still present, but migrate their `IRunUi` fakes to Foundation's opaque `DialogOwner` contract. The equivalent new tests are the WPF result/view-model tests from Task 3; do not delete the WinForms construction test in this phase.
+
+- [ ] **Step 2: Run the host-flow tests to verify they fail**
+
+ Run:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ShellBackupFlowTests|FullyQualifiedName~BackupResultTimelinePublicationTests|FullyQualifiedName~RunCoordinatorTests|FullyQualifiedName~ProgressPageViewTests"
+ ```
+
+ Expected: FAIL because `CreateSnapshotCommand` has not been wired, no injected run seam is available for the test, and Timeline is not refreshed/published after result completion.
+
+- [ ] **Step 3: Add the one WPF backup flow to ShellViewModel**
+
+ Add a real command to the compact top bar; it is unavailable while a run is active. Opening the selection workspace is not run admission. The start callback performs the only admission attempt and owns release in one `finally`:
+
+ ```csharp
+ private async Task StartBackupAsync(BackupRunRequest request)
+ {
+ if (!RunCoordinator.TryStart())
+ {
+ backupWorkspace?.ReportAdmissionRejected("Another backup or restore is already running.");
+ return;
+ }
+
+ BackupRunCompletion completion;
+ try
+ {
+ completion = await runBackupAsync(request);
+ }
+ finally
+ {
+ RunCoordinator.SetRunning(false);
+ }
+
+ completionPublisher.Publish(completion.AttemptedBackupPath, request.SnapshotName,
+ completion.Summary, DateTime.Now);
+ await timelineWorkspace.RefreshAsync();
+ NavigateTo(new ResultWorkspaceViewModel(completion.Summary, completion.Outcomes,
+ ReturnToTimelineAsync), "Snapshot result");
+ }
+
+ private async Task RunWpfBackupAsync(BackupRunRequest request)
+ {
+ var progress = new ProgressWorkspaceViewModel(dispatcher, ownerProvider,
+ runDialogService, dialogs);
+ NavigateTo(progress, "Creating snapshot");
+ RunSummary summary = await progress.RunBackupAsync(request);
+ return new BackupRunCompletion(summary, progress.Outcomes, progress.AttemptedBackupPath);
+ }
+
+ private Task ReturnToTimelineAsync()
+ {
+ ShowTimeline();
+ return Task.CompletedTask;
+ }
+ ```
+
+ In the production `ShellViewModel` composition constructor, create the one app-lifetime `SnapshotEventCatalog`, call `WpfAppRestoreDialog.Register(snapshotEventCatalog)` before accepting restore navigation, then initialize `runBackupAsync` to `RunWpfBackupAsync`. Its internal `ForTest` constructor accepts the run delegate and async Timeline-refresh delegate without registering the Core static callback, so host-flow tests have no disk or WPF-window dependency. `RunCoordinator.RunningChanged` marshals through the WPF dispatcher before it calls `CreateSnapshotCommand.RaiseCanExecuteChanged()`; inactive presentation is a convenience, never race prevention.
+
+ Add `CreateSnapshotCommand` to `MainWindow.xaml`'s top command bar with text and an automation name. It must open only the selection page—not start writes—and must not introduce a permanent sidebar.
+
+- [ ] **Step 4: Publish and return without retaining output or inventing snapshot state**
+
+ Call Task 1's publisher once per terminal backup before showing results, passing `completion.AttemptedBackupPath` copied from the runner's `BackupOutputPath`. The publisher canonicalizes that exact attempted path and calls `SnapshotEventCatalog.Read()`; when it sees an event there, regardless of `Verified`, `Partial`, `Failed`, or `Unreadable`, it leaves discovery as the sole event source. When no recognized retained event exists, it calls `RecordSessionFailure` with the terminal summary's real detail. It must never construct a path or call `Data.NowShort` after the run ends.
+
+ After publication, await the Timeline plan's `TimelineViewModel.RefreshAsync(CancellationToken cancellationToken = default)` once before showing the result. That method rereads the app-lifetime catalog and replaces its observable event collection; returning from the result only calls `ShowTimeline()` and does not open Compare automatically.
+
+- [ ] **Step 5: Run the host-flow tests to verify they pass**
+
+ Re-run the Step 2 command.
+
+
+ Expected: PASS. Exactly one run gains admission, a rejected second request leaves the active UI untouched, the completed/partial/failed display comes from the runner summary, Timeline receives a session failure only when disk cannot represent the attempt, and Back to Timeline shows the already refreshed one shared event model.
+
+- [ ] **Step 6: Perform the required real Windows smoke path while WinForms remains runnable**
+
+ Run the WPF application from a Windows desktop:
+
+ ```powershell
+ dotnet run --project src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj
+ ```
+
+ Expected: the Timeline home opens with the compact top bar and enabled **Create snapshot** when idle. Select **Create snapshot**; verify the six scopes, environment warning, preset buttons, destination browse, and None/Fast/Max choices are keyboard-reachable. Choose a safe empty destination outside every selected folder, capture one small supported scope, and observe Progress metrics/logs, pause/resume at a module boundary, and the archive phase disabling Cancel. Complete the capture; verify Result wording matches the actual summary; choose **Back to Timeline**; the new verified snapshot appears as selectable. Repeat with a deliberately canceled run only when a safe test destination is used: verify the result says incomplete only if the engine gave `RunSummary.Incomplete`, and Timeline shows either the discovered retained state or one session-only diagnostic failure—never a fabricated verified snapshot.
+
+ Then start the legacy application without deleting or modifying it:
+
+ ```powershell
+ dotnet run --project src/WinRestoreKit/WinRestoreKit.csproj
+ ```
+
+ Expected: WinForms still constructs and exposes its existing backup/progress flow. Do not run a concurrent backup from both shells; close it after confirming startup. This is side-by-side migration verification, not final cutover or publish verification.
+
+- [ ] **Step 7: Commit the integrated WPF flow**
+
+ ```powershell
+ git add src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs src/WinRestoreKit.Wpf/MainWindow.xaml src/WinRestoreKit.Wpf/ViewModels/BackupWorkspaceViewModel.cs src/WinRestoreKit.Wpf/ViewModels/BackupRunCompletion.cs src/WinRestoreKit.Tests/ShellBackupFlowTests.cs src/WinRestoreKit.Tests/BackupResultTimelinePublicationTests.cs src/WinRestoreKit.Tests/RunCoordinatorTests.cs src/WinRestoreKit.Tests/ProgressPageViewTests.cs
+ git commit -m "feat: complete WPF snapshot workflow"
+ ```
+
+## Final Verification Checklist
+
+- [ ] Run the focused commands from Tasks 1–5 and confirm each expected outcome, including Core-backed destination containment, archive, ownership, run-control, and summary regressions.
+- [ ] Run the full existing test suite only after all focused tests and the WPF smoke path succeed:
+
+ ```powershell
+ dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj
+ ```
+
+ Expected: PASS. Existing Core/module/payload/manifest/orchestration behavior remains covered; no test depends on `IWin32Window Owner` or `MessageBoxIcon` in Application contracts.
+
+- [ ] On a real Windows desktop, perform the Task 5 WPF smoke path and confirm the owner-bound app-reinstall dialog opens over the WPF owner, supports a selected source plus alternate discovered source, reports unreadable exports honestly, and asks to stop only after the current app.
+- [ ] Confirm WPF has no registry/payload parsing code: `AppRestoreService` is the sole app-export/payload preparation implementation; selection, progress, results, and XAML only bind structured values.
+- [ ] Confirm both WPF and WinForms remain runnable, there is no publish command, no removal of WinForms files, and no shipping-identity change in this plan. Those are Cutover work.
diff --git a/docs/superpowers/plans/2026-08-09-compare-confirm-restore.md b/docs/superpowers/plans/2026-08-09-compare-confirm-restore.md
new file mode 100644
index 0000000..b516f1d
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-compare-confirm-restore.md
@@ -0,0 +1,1812 @@
+# Compare, Confirm, and Restore Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Let a user compare any Timeline-selected verified or partial snapshot with the current PC, select only whole modules with usable captured artifacts, and start the unchanged safe restore pipeline through owner-bound WPF confirmation dialogs.
+
+**Architecture:** Keep all comparison evidence and module catalog projection in framework-neutral `WinRestoreKit.Application`; WPF consumes immutable evidence records and never reads manifests, payloads, registry exports, or artifacts. The Timeline transfers its prepared read scope to the Compare navigator; the workspace owns it until comparison completes or is cancelled, then disposes it. Confirm is a WPF presentation of declared restore impacts, while the existing Application `BackupRestoreOrchestrator` remains the sole authority for `RestorePlan`, `RestoreScope`, `SnapshotGate`, `RestoreDispatch`, result logging, and `ExplorerRestartPrompt`.
+
+**Tech Stack:** .NET 8 for Windows, C#, WPF/MVVM, `System.Threading`, xUnit 2.9.3, and the existing `WinRestoreKit.Core` restore contracts.
+
+## Global Constraints
+
+- Execute this plan after `2026-08-09-wpf-foundation-application-shell.md` and `2026-08-09-timeline-event-model.md`; do not cover backup creation, results migration, or final project cutover here.
+- Windows/.NET 8 only; preserve `WinRestoreKit.Core` backup/restore semantics and the on-disk snapshot and manifest formats.
+- `WinRestoreKit.Application` references neither WinForms nor WPF. It may use Core internals only through the Foundation-provided friend assembly relationship; WPF consumes Application public contracts rather than Core internal catalog or manifest types.
+- Keep WinForms runnable during this stage. Do not delete or alter the existing WinForms restore wizard/forms except for moving a duplicate test only when the WPF test covers the new Application behavior.
+- The four comparison states are exactly `ComparisonState { Changed, Same, Unavailable, NotCaptured }`. Do not replace `Unavailable` with `Same`, omit it from the default view, or invent before/after values.
+- Use manifest evidence before `BackupBase.HasArtifactIn(preparedPayloadPath)`, with the exact `RestoreContents` precedence: manifest `Succeeded` proves an artifact; manifest `Skipped` or `Failed` proves none; only an absent/silent/unknown manifest entry reaches `HasArtifactIn`. A `false` probe becomes `NotCaptured`; a `null` probe becomes `NotCaptured` when a manifest exists (its silence says the module was not in that run), but remains usable when no manifest exists—the same `RestoreContents` legacy fallback—so the subsequent drift result determines `Changed`, `Same`, or `Unavailable`. A thrown artifact probe is `Unavailable` for that module.
+- After a usable artifact is established, map `BackupBase.HasDriftedFrom(preparedPayloadPath)` `true` to `Changed`, `false` to `Same`, and `null` or an exception to `Unavailable`. One module error must log and affect only that row.
+- Comparison is read-only. It must not call `Backup`, `Restore`, `RestoreScope.HasBackup`, or write in the selected snapshot directory. A compressed read scope is temporary and must be disposed exactly once after every success, error, cancellation, or discarded snapshot selection.
+- Comparison runs at a bounded concurrency of four probes, honours cancellation before starting pending work, awaits already-running workers before releasing the payload, and preserves catalog order even when results complete out of order.
+- Restore selection is whole-module only. A row may enter the restore set only when `ModuleComparison.HasUsableArtifact` is true; an `Unavailable` comparison with a proven readable artifact remains selectable but must remain visibly unavailable.
+- The default filter is **All modules**. **Changed only** is an optional view filter applied after evidence arrives; it must not mutate comparison results or the restore set.
+- Changing to a different Timeline snapshot when the restore set is non-empty requires an owner-bound, default-cancel discard confirmation. Accepting clears the set before constructing the new workspace; declining disposes the incoming payload and keeps the old snapshot and set unchanged.
+- Do not create a semantic comparison-provider contract in this stage. Core has no current verified semantic values or item-level restore contract; the tray shows only captured-artifact evidence and existing `BackupBase` restore declarations.
+- Confirm shows only existing impacts: `RestoreTargets`, `ProcessesToCloseBeforeRestore`, `WarningMessage`, `RequiresExplorerRestart`, and `RestorePlan.FidelityCaveat`. Do not add a reboot field or infer a sign-out field by parsing warning text; display an existing warning verbatim when present.
+- The mandatory partial/failed pre-restore-snapshot decision remains at the existing `SnapshotGate` point immediately before restore writes. `IRunDialogService.ConfirmSnapshotOverride` must show an owner-bound Yes/No dialog whose default is No; WPF must never pre-answer or bypass it.
+- The Foundation move replaces `RunSummary.Icon` with `RunSeverity { Information, Warning, Error }` and replaces the WinForms-only owner with `IRunUi.DialogOwner` of type `object`. WPF supplies its main `Window` through that property for the existing Core `AppStoreApps.RestoreAsync(path, ui.DialogOwner)` / `RestoreDialog : Action` seam; do not add a duplicate shell app-restore dialog abstraction or compatibility overload.
+- Keep tests in `src/WinRestoreKit.Tests`; use Foundation's `WpfTestHost.Run(...)` STA helper for every test that constructs a WPF `Window`, `UserControl`, or dialog.
+
+---
+
+## File and Interface Map
+
+### Inputs from earlier plans
+
+```csharp
+// src/WinRestoreKit.Application/Snapshots/SnapshotEventKind.cs
+public enum SnapshotEventKind { Verified, Partial, Failed, Unreadable }
+
+// src/WinRestoreKit.Application/Snapshots/SnapshotEvent.cs
+public sealed class SnapshotEvent
+{
+ public SnapshotEventKind Kind { get; }
+ public DateTime Created { get; }
+ public string DisplayName { get; }
+ public string CanonicalPath { get; }
+ public string DiagnosticReason { get; }
+ public string MachineName { get; }
+ public long SizeBytes { get; }
+ public bool IsSizeComplete { get; }
+ public bool IsRestorable { get; } // true only for Verified and Partial
+ internal ManifestData Manifest { get; }
+}
+
+// src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparationService.cs
+public Task PrepareAsync(
+ SnapshotEvent snapshot, CancellationToken cancellationToken);
+
+// SnapshotPayloadPreparation is IDisposable. Error != null means no usable payload;
+// disposing it releases the Core BackupPayload.ReadScope, including temporary extraction.
+
+// src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs
+internal interface ITimelineNavigator
+{
+ void OpenCompare(SnapshotPayloadPreparation preparation);
+ void ShowSnapshotDiagnostic(SnapshotEvent snapshot);
+}
+```
+
+```csharp
+// Foundation-owned, in namespace WinRestoreKit.
+internal interface IRunUi
+{
+ void SetProgressText(string text);
+ void SetProgressPercent(int percent);
+ void SetProgressDetail(string groupInfo, string elapsed, string remaining,
+ string throughput, long bytesWritten, int errors, int warnings);
+ void ShowSummary(RunSummary summary, string caption,
+ IReadOnlyList outcomes);
+ object DialogOwner { get; }
+ IReadOnlyList ShowConsentDialog(RestorePlan plan); // null = cancel
+ bool ConfirmSnapshotOverride(string text, string caption); // false = do not continue
+ void ShowPlanCompositionError(string text, string caption);
+ void SetExplorerRestartVisible(bool visible);
+}
+
+internal sealed class BackupRestoreOrchestrator
+{
+ internal BackupRestoreOrchestrator(IRunUi ui, RunControl runControl = null);
+ internal Task RunRestore(IReadOnlyList modules, string backupPath);
+}
+
+// src/WinRestoreKit.Wpf/Services/IRunPresentation.cs
+internal interface IRunPresentation
+{
+ void SetProgressText(string text);
+ void SetProgressPercent(int percent);
+ void SetProgressDetail(string groupInfo, string elapsed, string remaining,
+ string throughput, long bytesWritten, int errors, int warnings);
+ void ShowSummary(RunSummary summary, string caption, IReadOnlyList outcomes);
+ void SetExplorerRestartVisible(bool visible);
+}
+
+// src/WinRestoreKit.Wpf/Services/IRunDialogService.cs
+internal interface IRunDialogService
+{
+ IReadOnlyList ShowRestoreConsent(RestorePlan plan);
+ bool ConfirmSnapshotOverride(string text, string caption);
+ void ShowPlanCompositionError(string text, string caption);
+}
+
+// src/WinRestoreKit.Wpf/Services/WpfRunUi.cs
+internal WpfRunUi(Dispatcher dispatcher, IRunPresentation presentation,
+ IRunDialogService dialogs, Func ownerProvider);
+```
+
+```csharp
+// Foundation-owned: src/WinRestoreKit.Wpf/Infrastructure/DelegateCommand.cs
+internal sealed class DelegateCommand : ICommand
+{
+ internal DelegateCommand(Action execute, Predicate canExecute = null);
+ public bool CanExecute(object parameter);
+ public void Execute(object parameter);
+ public event EventHandler CanExecuteChanged;
+ internal void RaiseCanExecuteChanged();
+}
+```
+
+Use this command and the adjacent Foundation `ObservableObject` from every Compare/Confirm ViewModel. Command delegates start a ViewModel-owned `async Task` wrapper that catches/reports its own errors; do not introduce another relay/async-command base type.
+
+### Files created by this plan
+
+| File | Responsibility |
+| --- | --- |
+| `src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs` | Public, immutable application projection of a registered Core module and its category. |
+| `src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs` | Produces catalog-order module projections without exposing Core's internal `ModuleCatalog`/`ModuleRegistration`. |
+| `src/WinRestoreKit.Application/Comparison/ComparisonState.cs` | The four immutable evidence-state names. |
+| `src/WinRestoreKit.Application/Comparison/ModuleComparison.cs` | Immutable per-module artifact and drift evidence consumed by WPF. |
+| `src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs` | Read-only manifest-first, cancellable, bounded-concurrency snapshot comparison. |
+| `src/WinRestoreKit.Wpf/ViewModels/ComparisonFilter.cs` | UI-only `All` / `ChangedOnly` filtering mode. |
+| `src/WinRestoreKit.Wpf/ViewModels/ModuleImpactViewModel.cs` | Direct projection of existing restore targets, process requirements, Explorer flag, and warning text. |
+| `src/WinRestoreKit.Wpf/ViewModels/ModuleComparisonRowViewModel.cs` | One ordered comparison row, its evidence state, detail data, and restore-set action. |
+| `src/WinRestoreKit.Wpf/ViewModels/RestoreSetViewModel.cs` | In-memory, whole-module restore selection with no persistence. |
+| `src/WinRestoreKit.Wpf/ViewModels/ComparisonWorkspaceViewModel.cs` | Compare lifecycle, filtering, cancellation, selected detail tray, and scope ownership. |
+| `src/WinRestoreKit.Wpf/ViewModels/ConfirmViewModel.cs` | Selected-module impact groups, restore launch, progress/result presentation, and cancellation request. |
+| `src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs` | `ITimelineNavigator` implementation that owns snapshot replacement/discard behavior and shell transitions. |
+| `src/WinRestoreKit.Wpf/Services/RestoreRunDialogService.cs` | Owner-bound implementation of the Foundation `IRunDialogService` for restore-specific dialogs. |
+| `src/WinRestoreKit.Wpf/Services/ICompareDialogService.cs` | Typed, testable compare navigation dialogs: discard restore set and display a snapshot diagnostic. |
+| `src/WinRestoreKit.Wpf/Services/CompareDialogService.cs` | Main-window-owned WPF implementation of `ICompareDialogService`; its explicit owner is passed to every modal diagnostic and discard confirmation. |
+| `src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml` and `.xaml.cs` | Accessible All/Changed-only workspace, ordered rows, temporary detail tray, and restore-set action. |
+| `src/WinRestoreKit.Wpf/Views/ConfirmView.xaml` and `.xaml.cs` | Impact-grouped confirmation screen and restore/progress controls. |
+| `src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml` and `.xaml.cs` | Owner-bound final consent dialog built from the actual `RestorePlan` created by the orchestrator. |
+| `src/WinRestoreKit.Tests/BackupModuleCatalogTests.cs` | Application projection order/category regression coverage. |
+| `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs` | Pure comparison state, precedence, isolation, ordering, cancellation, and cleanup tests. |
+| `src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs` | Whole-module restore-set rules. |
+| `src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs` | Filter, row-order, detail tray, and cancellation/selection behavior. |
+| `src/WinRestoreKit.Tests/ConfirmViewModelTests.cs` | Existing-impact grouping and real orchestrator-entry coverage. |
+| `src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs` | STA WPF construction, default-safe consent, and modal owner wiring. |
+
+### Files modified by this plan
+
+| File | Change |
+| --- | --- |
+| `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs` | Replace the Foundation-only Timeline placeholder with a `CurrentWorkflow` host and explicit Timeline/Compare/Confirm transitions. |
+| `src/WinRestoreKit.Wpf/MainWindow.xaml` | Add data templates for Timeline, Compare, and Confirm view models; retain the compact Foundation shell chrome. |
+| `src/WinRestoreKit.Wpf/MainWindow.xaml.cs` | Compose `CompareWorkflowNavigator` with the main window as the dialog owner and supply it to the Timeline view model. |
+
+### Contracts produced for later plans
+
+```csharp
+// src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs
+public sealed class BackupModuleRegistration
+{
+ public BackupBase Module { get; }
+ public string Category { get; }
+ public string Title { get; }
+}
+
+// src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs
+public static class BackupModuleCatalog
+{
+ public static IReadOnlyList CreateAll();
+}
+
+// src/WinRestoreKit.Application/Comparison/ComparisonState.cs
+public enum ComparisonState { Changed, Same, Unavailable, NotCaptured }
+
+// src/WinRestoreKit.Application/Comparison/ModuleComparison.cs
+public sealed class ModuleComparison
+{
+ public BackupBase Module { get; }
+ public ComparisonState State { get; }
+ public bool HasUsableArtifact { get; }
+ public string ArtifactSummary { get; }
+ public string Reason { get; }
+}
+
+// src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs
+public sealed class SnapshotComparisonService
+{
+ public Task> CompareAsync(
+ SnapshotEvent snapshot, IReadOnlyList modules,
+ CancellationToken cancellationToken);
+}
+```
+
+The service also has one **internal** overload accepting the Timeline-owned `SnapshotPayloadPreparation` and `IProgress`. It is a resource-ownership handoff, not a compatibility overload: the public exact signature creates/disposes its own preparation, while WPF uses the supplied scope so Timeline extraction is neither duplicated nor leaked.
+
+---
+
+### Task 1: Expose the module catalog to framework-neutral consumers
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs`
+- Create: `src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs`
+- Test: `src/WinRestoreKit.Tests/BackupModuleCatalogTests.cs`
+
+**Interfaces:**
+- Consumes: Core-internal `Conf.ModuleCatalog.CreateAll()` and `ModuleRegistration.Module`/`Category`, accessed only within the Foundation-created `WinRestoreKit.Application` friend assembly.
+- Produces: `BackupModuleCatalog.CreateAll()` returning one immutable `BackupModuleRegistration` per registered module, in the exact Core catalog order, with its existing category and `BackupBase.Title`.
+
+- [ ] **Step 1: Write the failing catalog-projection test**
+
+```csharp
+[Fact]
+public void CreateAll_PreservesCoreCatalogOrderCategoryAndTitle()
+{
+ IReadOnlyList actual = BackupModuleCatalog.CreateAll();
+ IReadOnlyList core = ModuleCatalog.CreateAll();
+
+ Assert.Equal(core.Count, actual.Count);
+ for (int index = 0; index < core.Count; index++)
+ {
+ Assert.Same(core[index].Module.GetType(), actual[index].Module.GetType());
+ Assert.Equal(core[index].Category, actual[index].Category);
+ Assert.Equal(core[index].Module.Title, actual[index].Title);
+ }
+}
+```
+
+- [ ] **Step 2: Run the test to verify it fails because the public Application catalog does not exist**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~BackupModuleCatalogTests"
+```
+
+Expected: compilation fails because `BackupModuleCatalog` and `BackupModuleRegistration` are undefined.
+
+- [ ] **Step 3: Add the immutable projection and order-preserving wrapper**
+
+```csharp
+// BackupModuleRegistration.cs
+namespace WinRestoreKit;
+
+public sealed class BackupModuleRegistration
+{
+ internal BackupModuleRegistration(BackupBase module, string category)
+ {
+ Module = module ?? throw new ArgumentNullException(nameof(module));
+ Category = category ?? string.Empty;
+ Title = module.Title ?? string.Empty;
+ }
+
+ public BackupBase Module { get; }
+ public string Category { get; }
+ public string Title { get; }
+}
+
+// BackupModuleCatalog.cs
+namespace WinRestoreKit;
+
+public static class BackupModuleCatalog
+{
+ public static IReadOnlyList CreateAll()
+ => ModuleCatalog.CreateAll()
+ .Select(entry => new BackupModuleRegistration(entry.Module, entry.Category))
+ .ToArray();
+}
+```
+
+Add the required `using Conf;`, `System`, `System.Collections.Generic`, and `System.Linq` directives rather than widening Core types or copying the catalog into WPF.
+
+- [ ] **Step 4: Run the focused test and verify the Core and Application catalog rows match**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~BackupModuleCatalogTests"
+```
+
+Expected: PASS; every module count, CLR type, category, and title matches the existing Core catalog in source order.
+
+- [ ] **Step 5: Commit the independently usable catalog facade**
+
+```powershell
+git add src/WinRestoreKit.Application/Modules/BackupModuleRegistration.cs src/WinRestoreKit.Application/Modules/BackupModuleCatalog.cs src/WinRestoreKit.Tests/BackupModuleCatalogTests.cs
+git commit -m "feat: expose application module catalog"
+```
+
+### Task 2: Define immutable evidence records and a manifest-first comparison service
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Comparison/ComparisonState.cs`
+- Create: `src/WinRestoreKit.Application/Comparison/ModuleComparison.cs`
+- Create: `src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+
+**Interfaces:**
+- Consumes: `SnapshotEvent` and its Application-internal parsed manifest; `SnapshotPayloadPreparationService`; `BackupBase.HasArtifactIn(string)`; `BackupBase.HasDriftedFrom(string)`; `BackupManifest.StateSucceeded`, `StateSkipped`, and `StateFailed`.
+- Produces: immutable, catalog-indexed `ModuleComparison` rows and the exact public `SnapshotComparisonService.CompareAsync(SnapshotEvent, IReadOnlyList, CancellationToken)` signature.
+- Invariant: a row can be restorable only when `HasUsableArtifact` is true. `ComparisonState.Unavailable` may still have a usable artifact when only the live drift probe is indeterminate; it must not be silently classified as `Same`.
+
+- [ ] **Step 1: Write failing state, precedence, and mapping tests**
+
+```csharp
+[Theory]
+[InlineData(true, ComparisonState.Changed)]
+[InlineData(false, ComparisonState.Same)]
+public async Task CompareAsync_ManifestSucceeded_MapsDriftWithoutArtifactProbe(
+ bool drifted, ComparisonState expected)
+{
+ ProbeModule module = new("Display", artifact: false, drifted: drifted);
+ SnapshotEvent snapshot = Snapshot(Succeeded(module));
+
+ ModuleComparison row = Assert.Single(await new SnapshotComparisonService()
+ .CompareAsync(snapshot, new[] { (BackupBase)module }, CancellationToken.None));
+
+ Assert.Equal(expected, row.State);
+ Assert.True(row.HasUsableArtifact);
+ Assert.Equal(0, module.ArtifactProbeCount);
+}
+
+[Theory]
+[InlineData(BackupManifest.StateSkipped)]
+[InlineData(BackupManifest.StateFailed)]
+public async Task CompareAsync_ManifestStatesWithoutArtifact_AreNotCaptured(string state)
+{
+ ProbeModule module = new("Fonts", artifact: true, drifted: true);
+
+ ModuleComparison row = Assert.Single(await new SnapshotComparisonService()
+ .CompareAsync(Snapshot(Entry(module, state)), new[] { (BackupBase)module }, CancellationToken.None));
+
+ Assert.Equal(ComparisonState.NotCaptured, row.State);
+ Assert.False(row.HasUsableArtifact);
+ Assert.Equal(0, module.ArtifactProbeCount);
+ Assert.Equal(0, module.DriftProbeCount);
+}
+
+[Fact]
+public async Task CompareAsync_ManifestSilentIndeterminateArtifact_IsNotCaptured()
+{
+ ProbeModule module = new("Terminal", artifact: null, drifted: false);
+
+ ModuleComparison row = Assert.Single(await new SnapshotComparisonService()
+ .CompareAsync(Snapshot(manifest: Manifest(EntryForDifferentModule())),
+ new[] { (BackupBase)module }, CancellationToken.None));
+
+ Assert.Equal(ComparisonState.NotCaptured, row.State);
+ Assert.False(row.HasUsableArtifact);
+ Assert.Equal(0, module.DriftProbeCount);
+}
+
+[Fact]
+public async Task CompareAsync_NoManifestIndeterminateArtifact_UsesRestoreContentsFallback()
+{
+ ProbeModule module = new("Legacy", artifact: null, drifted: false);
+
+ ModuleComparison row = Assert.Single(await new SnapshotComparisonService()
+ .CompareAsync(Snapshot(manifest: null), new[] { (BackupBase)module }, CancellationToken.None));
+
+ Assert.Equal(ComparisonState.Same, row.State);
+ Assert.True(row.HasUsableArtifact);
+ Assert.Equal(1, module.DriftProbeCount);
+}
+```
+
+In this test file, construct `SnapshotEvent` through Timeline's internal constructor under the Application-to-Tests friendship, create a real temporary backup folder for every snapshot, and use private `BackupBase` fakes only to observe the existing virtual probe seams. Do not use fake semantic values or add a semantic-provider interface.
+
+- [ ] **Step 2: Run the tests to verify the comparison types and service are missing**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests"
+```
+
+Expected: compilation fails because `ComparisonState`, `ModuleComparison`, and `SnapshotComparisonService` do not exist.
+
+- [ ] **Step 3: Add the state and immutable evidence record**
+
+```csharp
+namespace WinRestoreKit;
+
+public enum ComparisonState
+{
+ Changed,
+ Same,
+ Unavailable,
+ NotCaptured
+}
+
+public sealed class ModuleComparison
+{
+ internal ModuleComparison(BackupBase module, ComparisonState state,
+ bool hasUsableArtifact, string artifactSummary, string reason)
+ {
+ Module = module ?? throw new ArgumentNullException(nameof(module));
+ State = state;
+ HasUsableArtifact = hasUsableArtifact;
+ ArtifactSummary = artifactSummary ?? string.Empty;
+ Reason = reason ?? string.Empty;
+ }
+
+ public BackupBase Module { get; }
+ public ComparisonState State { get; }
+ public bool HasUsableArtifact { get; }
+ public string ArtifactSummary { get; }
+ public string Reason { get; }
+}
+```
+
+Keep construction internal so WPF can render evidence but cannot manufacture comparison results. The Tests assembly accesses it through Application's existing `InternalsVisibleTo` attribute.
+
+- [ ] **Step 4: Implement exact manifest-first presence and drift mapping**
+
+```csharp
+private static ModuleComparison CompareOne(BackupBase module, string payloadPath,
+ ManifestData manifest)
+{
+ ManifestModule entry = FindManifestEntry(manifest, module.GetType().Name);
+
+ if (entry?.State == BackupManifest.StateSkipped || entry?.State == BackupManifest.StateFailed)
+ {
+ return new ModuleComparison(module, ComparisonState.NotCaptured, false,
+ "The snapshot manifest records no usable artifact for this module.",
+ string.IsNullOrWhiteSpace(entry.Reason) ? entry.State : entry.Reason);
+ }
+
+ bool usableArtifact;
+ string artifactSummary;
+ if (entry?.State == BackupManifest.StateSucceeded)
+ {
+ usableArtifact = true;
+ artifactSummary = "The snapshot manifest records this module as captured.";
+ }
+ else
+ {
+ bool? probe;
+ try { probe = module.HasArtifactIn(payloadPath); }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.LogMessage("Comparison artifact probe failed for " + module.Title + ": " + ex.Message);
+ return new ModuleComparison(module, ComparisonState.Unavailable, false,
+ "Artifact presence could not be determined.", ex.Message);
+ }
+
+ if (probe == false)
+ return new ModuleComparison(module, ComparisonState.NotCaptured, false,
+ "The module proved that this snapshot has no restore artifact.", string.Empty);
+ if (!probe.HasValue)
+ {
+ if (manifest != null)
+ return new ModuleComparison(module, ComparisonState.NotCaptured, false,
+ "The manifest does not record this module and the module cannot prove an artifact.",
+ string.Empty);
+
+ // Preserve RestoreContents' no-manifest fallback; drift remains explicit below.
+ usableArtifact = true;
+ artifactSummary = "No manifest is available and the module cannot disprove a legacy artifact.";
+ }
+ else
+ {
+ usableArtifact = true;
+ artifactSummary = "The module verified a captured artifact.";
+ }
+
+ }
+
+ try
+ {
+ bool? drifted = module.HasDriftedFrom(payloadPath);
+ if (drifted == true)
+ return new ModuleComparison(module, ComparisonState.Changed, usableArtifact, artifactSummary,
+ "Core confirmed that current state differs from the snapshot.");
+ if (drifted == false)
+ return new ModuleComparison(module, ComparisonState.Same, usableArtifact, artifactSummary,
+ "Core confirmed that current state matches the snapshot.");
+ return new ModuleComparison(module, ComparisonState.Unavailable, usableArtifact, artifactSummary,
+ "Core could not establish a trustworthy live comparison.");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.LogMessage("Comparison drift probe failed for " + module.Title + ": " + ex.Message);
+ return new ModuleComparison(module, ComparisonState.Unavailable, usableArtifact, artifactSummary,
+ ex.Message);
+ }
+}
+```
+
+`FindManifestEntry` must match exact CLR type name with `StringComparison.Ordinal`, just as `RestoreContents` does. Treat every manifest state other than the three named states as silent and call `HasArtifactIn`; never claim a future/unknown state means captured.
+
+- [ ] **Step 5: Add the two ownership entry points without duplicating payload extraction**
+
+```csharp
+public sealed class SnapshotComparisonService
+{
+ public async Task> CompareAsync(
+ SnapshotEvent snapshot, IReadOnlyList modules,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(snapshot);
+ if (!snapshot.IsRestorable)
+ throw new ArgumentException("Only a verified or partial snapshot can be compared.", nameof(snapshot));
+
+ using SnapshotPayloadPreparation preparation = await new SnapshotPayloadPreparationService()
+ .PrepareAsync(snapshot, cancellationToken).ConfigureAwait(false);
+ return await CompareAsync(preparation, modules, cancellationToken, progress: null)
+ .ConfigureAwait(false);
+ }
+
+ internal Task> CompareAsync(
+ SnapshotPayloadPreparation preparation, IReadOnlyList modules,
+ CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(preparation);
+ // This overload never disposes preparation: the Timeline navigator transferred ownership
+ // to the WPF workspace, which disposes it only after this returned task has settled.
+ return ComparePreparedAsync(preparation, modules, cancellationToken, progress);
+ }
+}
+```
+
+When `preparation.Error` is non-empty, return `NotCaptured` only for manifest `Skipped`/`Failed` rows; every other module becomes `Unavailable` with that exact preparation error and `HasUsableArtifact == false`. This exposes a missing/corrupt payload as a real disabled comparison/restore condition rather than treating the snapshot as empty.
+
+- [ ] **Step 6: Run the mapping suite and verify every evidence state is honest**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests"
+```
+
+Expected: PASS; manifest-success rows bypass `HasArtifactIn`, skipped/failed rows are `NotCaptured`, proven absence is `NotCaptured`, a silent manifest plus indeterminate artifact is `NotCaptured`, no-manifest indeterminacy preserves the `RestoreContents` fallback and remains subject to drift, and throwing artifact or drift probes are `Unavailable` without producing a false `Same` row.
+
+- [ ] **Step 7: Commit the evidence model and basic service behavior**
+
+```powershell
+git add src/WinRestoreKit.Application/Comparison/ComparisonState.cs src/WinRestoreKit.Application/Comparison/ModuleComparison.cs src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs
+git commit -m "feat: compare selected snapshot modules"
+```
+
+### Task 3: Make comparison bounded, ordered, cancellable, and cleanup-safe
+
+**Files:**
+- Modify: `src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs`
+- Modify: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+
+**Interfaces:**
+- Consumes: Task 2 comparison records and Timeline's disposable `SnapshotPayloadPreparation`.
+- Produces: `internal ComparisonProgress(int ordinal, ModuleComparison comparison)` and the existing public comparison method, now with bounded worker scheduling and proven cleanup.
+- Invariant: `Task.WhenAll` result position and progress `Ordinal` equal the input catalog position. Cancellation waits for all in-flight synchronous `BackupBase` probes to finish before the caller can dispose the prepared payload.
+
+- [ ] **Step 1: Add failing isolation, order, concurrency, cancellation, and extraction-cleanup tests**
+
+```csharp
+[Fact]
+public async Task CompareAsync_OneThrowingModule_DoesNotAbortLaterCatalogRows()
+{
+ ProbeModule broken = new("Broken", artifact: true, drifted: null) { ThrowOnDrift = true };
+ ProbeModule same = new("Same", artifact: true, drifted: false);
+
+ IReadOnlyList rows = await Compare(new[] { broken, same });
+
+ Assert.Collection(rows,
+ row => Assert.Equal(ComparisonState.Unavailable, row.State),
+ row => Assert.Equal(ComparisonState.Same, row.State));
+}
+
+[Fact]
+public async Task CompareAsync_BoundsProbeConcurrencyAndReturnsCatalogOrder()
+{
+ ConcurrentProbeModule[] modules = Enumerable.Range(0, 9)
+ .Select(index => new ConcurrentProbeModule("Module " + index)).ToArray();
+
+ IReadOnlyList rows = await Compare(modules);
+
+ Assert.True(ConcurrentProbeModule.MaximumObserved <= 4);
+ Assert.Equal(modules.Select(module => module.Title), rows.Select(row => row.Module.Title));
+}
+
+[Fact]
+public async Task CompareAsync_CancellationWaitsForWorkersThenDeletesCompressedExtraction()
+{
+ string backup = CreateCompressedBackupWithArtifact();
+ BlockingProbeModule module = new("Blocking");
+ using CancellationTokenSource cancellation = new();
+ SnapshotComparisonService service = new();
+
+ Task> task = service.CompareAsync(
+ Snapshot(backup, manifest: null), new[] { (BackupBase)module }, cancellation.Token);
+ await module.Started.Task;
+ cancellation.Cancel();
+ module.Release.Set();
+
+ await Assert.ThrowsAsync(() => task);
+ Assert.Empty(Directory.EnumerateDirectories(Path.Combine(Path.GetTempPath(), "WinRestoreKit"),
+ "payload-*", SearchOption.TopDirectoryOnly));
+}
+```
+
+The cleanup test must use a real `payload.zip`, wait until the fake's synchronous `HasDriftedFrom` starts, cancel, release it, and only then assert extraction cleanup. It must not assume cancellation can interrupt module code that does not accept a token.
+
+- [ ] **Step 2: Run the focused suite and verify the missing bounded/cancellation behavior fails**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests"
+```
+
+Expected: FAIL because workers are not yet bounded/cancellation-aware and the compressed temporary extraction lifetime is not proved.
+
+- [ ] **Step 3: Implement four-worker scheduling, indexed progress, and cancellation settlement**
+
+```csharp
+internal readonly struct ComparisonProgress
+{
+ internal ComparisonProgress(int ordinal, ModuleComparison comparison)
+ {
+ Ordinal = ordinal;
+ Comparison = comparison;
+ }
+
+ internal int Ordinal { get; }
+ internal ModuleComparison Comparison { get; }
+}
+
+private const int MaximumConcurrentProbes = 4;
+
+private async Task> ComparePreparedAsync(
+ SnapshotPayloadPreparation preparation, IReadOnlyList modules,
+ CancellationToken cancellationToken, IProgress progress)
+{
+ BackupBase[] catalog = (modules ?? Array.Empty())
+ .Where(module => module != null).ToArray();
+
+ if (!string.IsNullOrWhiteSpace(preparation.Error))
+ return PayloadFailureRows(catalog, preparation.Snapshot.Manifest, preparation.Error);
+
+ using SemaphoreSlim gate = new(MaximumConcurrentProbes, MaximumConcurrentProbes);
+ Task[] workers = catalog.Select((module, ordinal) =>
+ CompareAtOrdinalAsync(module, ordinal, preparation.Path, preparation.Snapshot.Manifest,
+ gate, cancellationToken, progress)).ToArray();
+
+ try
+ {
+ return await Task.WhenAll(workers).ConfigureAwait(false);
+ }
+ finally
+ {
+ // Await active Task.Run probes before allowing the caller's finally/using to remove the
+ // prepared payload. Suppress their already-reported exceptions while preserving cancellation.
+ await Task.WhenAll(workers.Select(ObserveCompletionAsync)).ConfigureAwait(false);
+ }
+}
+
+private static async Task CompareAtOrdinalAsync(
+ BackupBase module, int ordinal, string payloadPath, ManifestData manifest,
+ SemaphoreSlim gate, CancellationToken token, IProgress progress)
+{
+ bool enteredGate = false;
+ try
+ {
+ await gate.WaitAsync(token).ConfigureAwait(false);
+ enteredGate = true;
+ token.ThrowIfCancellationRequested();
+ ModuleComparison row = await Task.Run(() => CompareOne(module, payloadPath, manifest), token)
+ .ConfigureAwait(false);
+ progress?.Report(new ComparisonProgress(ordinal, row));
+ return row;
+ }
+ finally
+ {
+ if (enteredGate)
+ gate.Release();
+ }
+}
+
+private static async Task ObserveCompletionAsync(Task task)
+{
+ try { await task.ConfigureAwait(false); }
+ catch (OperationCanceledException) { }
+ catch (Exception) { }
+}
+```
+
+Do not use `Parallel.ForEach`, an unbounded `Task.Run` fan-out, `Task.WaitAll`, or a continuation that disposes the scope independently. `Task.WhenAll` preserves the task-array order; `ComparisonProgress.Ordinal` lets WPF update the matching already-created row without sorting completion order.
+
+- [ ] **Step 4: Run the focused suite and verify cancellation and cleanup pass**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests"
+```
+
+Expected: PASS; no more than four probes run at once, one broken module leaves later evidence intact, cancellation is observed, and the temporary archive extraction is removed only after active workers settle.
+
+- [ ] **Step 5: Commit the operational comparison guarantees**
+
+```powershell
+git add src/WinRestoreKit.Application/Comparison/SnapshotComparisonService.cs src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs
+git commit -m "feat: make snapshot comparison cancellable"
+```
+
+### Task 4: Build the WPF Compare workspace, restore set, and safe snapshot replacement
+
+**Files:**
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ComparisonFilter.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ModuleImpactViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ModuleComparisonRowViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/RestoreSetViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ComparisonWorkspaceViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/ICompareDialogService.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/CompareDialogService.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml.cs`
+- Modify: `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs`
+- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml`
+- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml.cs`
+- Test: `src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs`
+
+**Interfaces:**
+- Consumes: `ITimelineNavigator.OpenCompare(SnapshotPayloadPreparation)`, `BackupModuleCatalog.CreateAll()`, Task 3's internal prepared-payload comparison overload, and Timeline's `SnapshotEvent` restorable/diagnostic facts.
+- Produces: WPF `ComparisonWorkspaceViewModel` with `Rows`, `VisibleRows`, `RestoreSet`, `SelectedRow`, `SelectedFilter`, `StartAsync`, `CancelAsync`, and `ContinueToConfirmCommand`; `CompareWorkflowNavigator` implements the exact Timeline navigation seam and owns the Confirm transition.
+- Invariant: `RestoreSet` holds `BackupBase` references only in memory. It neither serializes values nor interprets an `Unavailable` row as `Same`.
+- Produces for Task 6: `CompareWorkflowNavigator.CurrentWorkspace` and `Task PendingTransition`, an internal settled-transition observation seam; Timeline continues to call only `void OpenCompare(...)`.
+- [ ] **Step 1: Write failing restore-set and workspace behavior tests under STA**
+
+```csharp
+[Fact]
+public void RestoreSet_OnlyAcceptsRowsWithUsableArtifacts()
+{
+ WpfTestHost.Run(() =>
+ {
+ RestoreSetViewModel restoreSet = new();
+ ModuleComparison unavailableButUsable = Comparison("Terminal", ComparisonState.Unavailable, true);
+ ModuleComparison absent = Comparison("Fonts", ComparisonState.NotCaptured, false);
+
+ restoreSet.Add(unavailableButUsable);
+ restoreSet.Add(absent);
+
+ Assert.Single(restoreSet.Modules);
+ Assert.Same(unavailableButUsable.Module, restoreSet.Modules[0]);
+ Assert.False(restoreSet.Contains(absent.Module));
+ });
+}
+
+[Fact]
+public void Workspace_DefaultsToAllAndChangedOnlyDoesNotChangeRestoreSet()
+{
+ WpfTestHost.Run(() =>
+ {
+ ComparisonWorkspaceViewModel workspace = LoadedWorkspace(
+ Comparison("Changed", ComparisonState.Changed, true),
+ Comparison("Same", ComparisonState.Same, true),
+ Comparison("Unknown", ComparisonState.Unavailable, true),
+ Comparison("Absent", ComparisonState.NotCaptured, false));
+
+ Assert.Equal(ComparisonFilter.All, workspace.SelectedFilter);
+ Assert.Equal(4, workspace.VisibleRows.Count);
+ workspace.RestoreSet.Add(workspace.Rows[2].Comparison);
+
+ workspace.SelectedFilter = ComparisonFilter.ChangedOnly;
+
+ Assert.Single(workspace.VisibleRows);
+ Assert.Equal("Changed", workspace.VisibleRows[0].Title);
+ Assert.True(workspace.RestoreSet.Contains(workspace.Rows[2].Comparison.Module));
+ });
+}
+
+[Fact]
+public void Workspace_SelectedRowExposesOnlyDeclaredImpacts()
+{
+ WpfTestHost.Run(() =>
+ {
+ ComparisonWorkspaceViewModel workspace = LoadedWorkspace(ComparisonWithDeclaredImpacts());
+ workspace.SelectedRow = workspace.Rows[0];
+
+ Assert.True(workspace.IsDetailTrayOpen);
+ Assert.Equal("Settings", workspace.SelectedRow.Category);
+ Assert.Contains(workspace.SelectedRow.Impact.Targets,
+ item => item.Kind == RestoreTargetKind.RegistryKey);
+ Assert.Contains(workspace.SelectedRow.Impact.Processes,
+ item => item.NeedsConsent && item.DisplayName == "Visual Studio Code");
+ Assert.True(workspace.SelectedRow.Impact.RequiresExplorerRestart);
+ Assert.Equal("Existing module warning.", workspace.SelectedRow.Impact.WarningMessage);
+ });
+}
+
+[Fact]
+public void Workspace_ContinueToConfirmUsesTheCurrentWholeModuleRestoreSet()
+{
+ WpfTestHost.Run(() =>
+ {
+ SnapshotEvent receivedSnapshot = null;
+ IReadOnlyList receivedModules = null;
+ ComparisonWorkspaceViewModel workspace = LoadedWorkspace(
+ (snapshot, modules) => { receivedSnapshot = snapshot; receivedModules = modules; },
+ Comparison("Changed", ComparisonState.Changed, true));
+ workspace.RestoreSet.Add(workspace.Rows[0].Comparison);
+
+ workspace.ContinueToConfirmCommand.Execute(null);
+
+ Assert.Same(workspace.Snapshot, receivedSnapshot);
+ Assert.Single(receivedModules);
+ Assert.Same(workspace.Rows[0].Comparison.Module, receivedModules[0]);
+ });
+}
+```
+
+Use a small `BackupBase` test module whose overrides return actual `RestoreTarget` and `RestoreCloseRequirement` objects. The assertions must inspect those values directly; do not create guessed values such as “reboot required.” Extend `LoadedWorkspace` to accept the optional `Action>` callback passed to the workspace constructor; its normal test overload passes a no-op action.
+
+- [ ] **Step 2: Run the tests to verify the ViewModels and WPF host are absent**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~RestoreSetViewModelTests|FullyQualifiedName~ComparisonWorkspaceViewModelTests"
+```
+
+Expected: compilation fails because the Compare ViewModels and `WpfTestHost`-consuming test surface do not yet exist.
+
+- [ ] **Step 3: Implement whole-module restore selection and direct impact projection**
+
+```csharp
+internal sealed class RestoreSetViewModel : ObservableObject
+{
+ private readonly ObservableCollection modules = new();
+
+ public ReadOnlyObservableCollection Modules { get; }
+
+ internal RestoreSetViewModel()
+ => Modules = new ReadOnlyObservableCollection(modules);
+
+ internal bool Contains(BackupBase module) => modules.Contains(module);
+ public bool HasItems => modules.Count != 0;
+
+ internal void Add(ModuleComparison comparison)
+ {
+ if (comparison == null || !comparison.HasUsableArtifact || modules.Contains(comparison.Module))
+ return;
+ modules.Add(comparison.Module);
+ OnPropertyChanged(nameof(HasItems));
+ }
+
+ internal void Remove(BackupBase module)
+ {
+ if (modules.Remove(module))
+ OnPropertyChanged(nameof(HasItems));
+ }
+
+ internal void Clear()
+ {
+ if (modules.Count == 0)
+ return;
+ modules.Clear();
+ OnPropertyChanged(nameof(HasItems));
+ }
+}
+
+internal sealed class ModuleImpactViewModel
+{
+ internal ModuleImpactViewModel(BackupBase module)
+ {
+ Targets = (module.RestoreTargets ?? Array.Empty()).ToArray();
+ Processes = (module.ProcessesToCloseBeforeRestore ?? Array.Empty())
+ .Where(requirement => requirement != null).ToArray();
+ RequiresExplorerRestart = module.RequiresExplorerRestart;
+ WarningMessage = module.WarningMessage ?? string.Empty;
+ }
+
+ public IReadOnlyList Targets { get; }
+ public IReadOnlyList Processes { get; }
+ public bool RequiresExplorerRestart { get; }
+ public string WarningMessage { get; }
+}
+
+internal enum ComparisonFilter { All, ChangedOnly }
+
+internal sealed class ModuleComparisonRowViewModel : ObservableObject
+{
+ private readonly RestoreSetViewModel restoreSet;
+
+ internal ModuleComparisonRowViewModel(BackupModuleRegistration registration,
+ RestoreSetViewModel restoreSet)
+ {
+ Registration = registration ?? throw new ArgumentNullException(nameof(registration));
+ this.restoreSet = restoreSet ?? throw new ArgumentNullException(nameof(restoreSet));
+ Impact = new ModuleImpactViewModel(registration.Module);
+ ToggleRestoreSetCommand = new DelegateCommand(_ => ToggleRestoreSet(), _ => CanChangeRestoreSet);
+ }
+
+ internal BackupModuleRegistration Registration { get; }
+ internal ModuleComparison Comparison { get; private set; }
+ public string Title => Registration.Title;
+ public string Category => Registration.Category;
+ public ModuleImpactViewModel Impact { get; }
+ public bool IsChecking => Comparison == null;
+ public bool CanChangeRestoreSet => Comparison?.HasUsableArtifact == true;
+ public bool IsInRestoreSet => restoreSet.Contains(Registration.Module);
+ public string StateLabel => IsChecking ? "Checking" : Comparison.State.ToString();
+ public string ArtifactSummary => IsChecking ? "Comparison has not finished." : Comparison.ArtifactSummary;
+ public string Reason => Comparison?.Reason ?? string.Empty;
+ public string RestoreActionLabel => IsInRestoreSet ? "Remove from restore" : "Add to restore";
+ public DelegateCommand ToggleRestoreSetCommand { get; }
+
+ internal void Apply(ModuleComparison comparison)
+ {
+ Comparison = comparison ?? throw new ArgumentNullException(nameof(comparison));
+ OnPropertyChanged(nameof(IsChecking));
+ OnPropertyChanged(nameof(StateLabel));
+ OnPropertyChanged(nameof(ArtifactSummary));
+ OnPropertyChanged(nameof(Reason));
+ OnPropertyChanged(nameof(CanChangeRestoreSet));
+ ToggleRestoreSetCommand.RaiseCanExecuteChanged();
+ }
+
+ private void ToggleRestoreSet()
+ {
+ if (!CanChangeRestoreSet)
+ return;
+ if (IsInRestoreSet)
+ restoreSet.Remove(Registration.Module);
+ else
+ restoreSet.Add(Comparison);
+ OnPropertyChanged(nameof(IsInRestoreSet));
+ OnPropertyChanged(nameof(RestoreActionLabel));
+ }
+}
+```
+
+Keep `RestoreTarget` values typed in the view model and bind their existing `Kind` and `Path`; do not repeat `RestorePlan` wording or parse `WarningMessage` to manufacture impact categories.
+
+- [ ] **Step 4: Implement workspace ownership, ordered streaming, and filtering**
+
+```csharp
+internal sealed class ComparisonWorkspaceViewModel : ObservableObject
+{
+ private readonly IReadOnlyList registrations;
+ private readonly SnapshotComparisonService comparisonService;
+ private readonly CancellationTokenSource comparisonCancellation = new();
+ private Task comparisonTask;
+ private ComparisonFilter selectedFilter = ComparisonFilter.All;
+ private ModuleComparisonRowViewModel selectedRow;
+ private readonly Action> showConfirm;
+
+ internal ComparisonWorkspaceViewModel(SnapshotEvent snapshot,
+ IReadOnlyList registrations,
+ SnapshotComparisonService comparisonService,
+ Action> showConfirm)
+ {
+ Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
+ this.registrations = registrations ?? throw new ArgumentNullException(nameof(registrations));
+ this.comparisonService = comparisonService ?? throw new ArgumentNullException(nameof(comparisonService));
+ this.showConfirm = showConfirm ?? throw new ArgumentNullException(nameof(showConfirm));
+ Rows = new ObservableCollection();
+ VisibleRows = new ObservableCollection();
+ RestoreSet = new RestoreSetViewModel();
+ RestoreSet.PropertyChanged += (_, e) =>
+ {
+ if (e.PropertyName == nameof(RestoreSet.HasItems))
+ {
+ OnPropertyChanged(nameof(CanContinueToConfirm));
+ ContinueToConfirmCommand.RaiseCanExecuteChanged();
+ }
+ };
+ ContinueToConfirmCommand = new DelegateCommand(_ => ContinueToConfirm(), _ => CanContinueToConfirm);
+ }
+
+ public SnapshotEvent Snapshot { get; }
+ public ObservableCollection Rows { get; }
+ public ObservableCollection VisibleRows { get; }
+ public RestoreSetViewModel RestoreSet { get; }
+ public bool IsComparing { get; private set; }
+ public string ComparisonStatus { get; private set; } = string.Empty;
+ public ComparisonFilter SelectedFilter
+ {
+ get => selectedFilter;
+ set { selectedFilter = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsAllFilter)); OnPropertyChanged(nameof(IsChangedOnlyFilter)); RefreshVisibleRows(); }
+ }
+ public bool IsAllFilter
+ {
+ get => SelectedFilter == ComparisonFilter.All;
+ set { if (value) SelectedFilter = ComparisonFilter.All; }
+ }
+ public bool IsChangedOnlyFilter
+ {
+ get => SelectedFilter == ComparisonFilter.ChangedOnly;
+ set { if (value) SelectedFilter = ComparisonFilter.ChangedOnly; }
+ }
+ public ModuleComparisonRowViewModel SelectedRow
+ {
+ get => selectedRow;
+ set { selectedRow = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsDetailTrayOpen)); }
+ }
+ public bool IsDetailTrayOpen => SelectedRow != null;
+ public bool CanContinueToConfirm => RestoreSet.HasItems && !IsComparing;
+ public DelegateCommand ContinueToConfirmCommand { get; }
+
+ internal Task StartAsync(SnapshotPayloadPreparation preparation)
+ => comparisonTask ??= StartCoreAsync(preparation);
+
+ private async Task StartCoreAsync(SnapshotPayloadPreparation preparation)
+ {
+ ArgumentNullException.ThrowIfNull(preparation);
+ Rows.Clear();
+ foreach (BackupModuleRegistration registration in registrations)
+ Rows.Add(new ModuleComparisonRowViewModel(registration, RestoreSet));
+ RefreshVisibleRows();
+
+ IsComparing = true;
+ OnPropertyChanged(nameof(IsComparing));
+ OnPropertyChanged(nameof(CanContinueToConfirm));
+ ContinueToConfirmCommand.RaiseCanExecuteChanged();
+ try
+ {
+ IProgress progress = new Progress(item =>
+ {
+ Rows[item.Ordinal].Apply(item.Comparison);
+ RefreshVisibleRows();
+ });
+ await comparisonService.CompareAsync(preparation,
+ registrations.Select(registration => registration.Module).ToArray(),
+ comparisonCancellation.Token, progress);
+ }
+ catch (OperationCanceledException) when (comparisonCancellation.IsCancellationRequested)
+ {
+ ComparisonStatus = "Comparison canceled.";
+ }
+ finally
+ {
+ IsComparing = false;
+ OnPropertyChanged(nameof(IsComparing));
+ OnPropertyChanged(nameof(CanContinueToConfirm));
+ ContinueToConfirmCommand.RaiseCanExecuteChanged();
+ preparation.Dispose();
+ RefreshVisibleRows();
+ }
+ }
+
+ internal async Task CancelAsync()
+ {
+ comparisonCancellation.Cancel();
+ if (comparisonTask != null)
+ await comparisonTask;
+ }
+
+ private void ContinueToConfirm()
+ {
+ if (CanContinueToConfirm)
+ showConfirm(Snapshot, RestoreSet.Modules.ToArray());
+ }
+
+ private void RefreshVisibleRows()
+ {
+ VisibleRows.Clear();
+ foreach (ModuleComparisonRowViewModel row in Rows)
+ if (SelectedFilter == ComparisonFilter.All ||
+ row.Comparison?.State == ComparisonState.Changed)
+ VisibleRows.Add(row);
+ }
+}
+```
+
+Set `SelectedFilter = ComparisonFilter.All` in the constructor. `Rows` is created once in catalog order before any result is reported; each progress update replaces only the matching ordinal's checking placeholder, so a completion race cannot reorder the list. `CancelAsync` must be awaited by the navigator before it discards a workspace; do not call `Dispose` from a continuation while a probe can still read the payload.
+
+- [ ] **Step 5: Implement the Timeline navigator and snapshot-change confirmation**
+
+```csharp
+internal interface ICompareDialogService
+{
+ bool ConfirmDiscardRestoreSet(Window owner, SnapshotEvent current, SnapshotEvent incoming);
+ void ShowSnapshotDiagnostic(Window owner, SnapshotEvent snapshot);
+}
+
+internal sealed class CompareDialogService : ICompareDialogService
+{
+ public bool ConfirmDiscardRestoreSet(Window owner, SnapshotEvent current, SnapshotEvent incoming)
+ => MessageBox.Show(owner,
+ "Changing from \"" + current.DisplayName + "\" to \"" + incoming.DisplayName +
+ "\" clears the selected restore modules. Change snapshot?",
+ "Change snapshot", MessageBoxButton.YesNo, MessageBoxImage.Warning,
+ MessageBoxResult.No) == MessageBoxResult.Yes;
+
+ public void ShowSnapshotDiagnostic(Window owner, SnapshotEvent snapshot)
+ => MessageBox.Show(owner, snapshot.DiagnosticReason, "Snapshot diagnostic",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+}
+
+internal sealed class CompareWorkflowNavigator : ITimelineNavigator
+{
+ private readonly ShellViewModel shell;
+ private readonly Window owner;
+ private readonly ICompareDialogService dialogs;
+ private ComparisonWorkspaceViewModel currentWorkspace;
+
+ internal CompareWorkflowNavigator(ShellViewModel shell, Window owner, ICompareDialogService dialogs)
+ {
+ this.shell = shell ?? throw new ArgumentNullException(nameof(shell));
+ this.owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ this.dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
+ }
+
+ internal Task PendingTransition { get; private set; } = Task.CompletedTask;
+ internal ComparisonWorkspaceViewModel CurrentWorkspace => currentWorkspace;
+
+ public void OpenCompare(SnapshotPayloadPreparation incoming)
+ {
+ if (string.Equals(currentWorkspace?.Snapshot.CanonicalPath, incoming.Snapshot.CanonicalPath,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ incoming.Dispose();
+ return;
+ }
+
+ if (currentWorkspace?.RestoreSet.HasItems == true &&
+ !dialogs.ConfirmDiscardRestoreSet(owner, currentWorkspace.Snapshot, incoming.Snapshot))
+ {
+ incoming.Dispose();
+ return;
+ }
+
+ PendingTransition = ReplaceWorkspaceAsync(incoming);
+ }
+
+ public void ShowSnapshotDiagnostic(SnapshotEvent snapshot)
+ => dialogs.ShowSnapshotDiagnostic(owner, snapshot);
+
+ private async Task ReplaceWorkspaceAsync(SnapshotPayloadPreparation incoming)
+ {
+ bool ownershipTransferred = false;
+ try
+ {
+ if (currentWorkspace != null)
+ await currentWorkspace.CancelAsync();
+ currentWorkspace?.RestoreSet.Clear();
+
+ currentWorkspace = new ComparisonWorkspaceViewModel(incoming.Snapshot,
+ BackupModuleCatalog.CreateAll(), new SnapshotComparisonService(), ShowConfirm);
+ ownershipTransferred = true;
+ shell.ShowCompare(currentWorkspace);
+ await currentWorkspace.StartAsync(incoming);
+ }
+ catch (Exception ex)
+ {
+ if (!ownershipTransferred)
+ incoming.Dispose();
+ shell.ShowInlineWorkflowError("Comparison could not start: " + ex.Message);
+ }
+ }
+
+ private void ShowConfirm(SnapshotEvent snapshot, IReadOnlyList modules)
+ {
+ ConfirmViewModel confirm = new(snapshot, modules, () => shell.ShowCompare(currentWorkspace));
+ shell.ShowConfirm(confirm);
+ }
+}
+```
+
+`ConfirmDiscardRestoreSet` must be a modal WPF dialog with the main window owner, **Cancel** as its default, and text that names both snapshot display names. Accepting clears the old in-memory set only after the user approves; rejecting leaves the old workspace/set untouched and releases only the incoming scope. The fire-and-forget interface boundary is contained here because Timeline's established `ITimelineNavigator` is `void`; `ReplaceWorkspaceAsync` catches every exception and renders it inline rather than allowing an unobserved task failure.
+
+Add `ShellViewModel.CurrentWorkflow`, `ShowTimeline()`, `ShowCompare(ComparisonWorkspaceViewModel)`, and `ShowConfirm(ConfirmViewModel)`, then bind `MainWindow`'s central `ContentControl` through explicit DataTemplates. `MainWindow` constructs `CompareWorkflowNavigator` with itself as `owner` and passes it to Timeline's view-model composition; the ViewModel itself never discovers or stores a WPF `Window`.
+
+- [ ] **Step 6: Add accessible Compare XAML with All as the default visible filter**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Provide item templates that render each existing `RestoreTarget.Kind` and `RestoreTarget.Path`, plus each existing `RestoreCloseRequirement.DisplayName` and `NeedsConsent` value. A row in its checking phase is labelled “Checking”; after comparison it has exactly one of the four required evidence states. Do not show a semantic before/after column, reboot line, or parsed registry/payload text.
+
+- [ ] **Step 7: Run the STA ViewModel suite and verify visible selection behavior**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~RestoreSetViewModelTests|FullyQualifiedName~ComparisonWorkspaceViewModelTests"
+```
+
+Expected: PASS; all four rows are visible by default, Changed only reduces only the view, usable-but-unavailable evidence may be selected as a whole module, NotCaptured cannot be selected, and the detail model contains only declared Core impacts.
+
+- [ ] **Step 8: Commit the Compare workspace and safe selection lifecycle**
+
+```powershell
+git add src/WinRestoreKit.Wpf/ViewModels src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs src/WinRestoreKit.Wpf/Services/ICompareDialogService.cs src/WinRestoreKit.Wpf/Services/CompareDialogService.cs src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml.cs src/WinRestoreKit.Wpf/MainWindow.xaml src/WinRestoreKit.Wpf/MainWindow.xaml.cs src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs
+git commit -m "feat: add WPF snapshot comparison workspace"
+```
+
+### Task 5: Add Confirm, owner-bound consent, and the unchanged restore pipeline
+
+**Files:**
+- Create: `src/WinRestoreKit.Wpf/ViewModels/ConfirmViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/RestoreRunDialogService.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/ConfirmView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/ConfirmView.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml.cs`
+- Modify: `src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs`
+- Modify: `src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs`
+- Modify: `src/WinRestoreKit.Tests/WpfTestHost.cs`
+- Test: `src/WinRestoreKit.Tests/ConfirmViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs`
+
+**Interfaces:**
+- Consumes: `RestoreSetViewModel.Modules`; existing `RestoreTargets`, `ProcessesToCloseBeforeRestore`, `WarningMessage`, `RequiresExplorerRestart`, `RestorePlan.FidelityCaveat`; Foundation's `IRunPresentation`, `IRunDialogService`, `WpfRunUi`, `WpfLogSink`, `RunCoordinator`, `RunControl`, and moved `BackupRestoreOrchestrator`.
+- Produces: a Confirm screen that presents selected modules grouped by existing process/Explorer impact and starts the original restore flow; a final WPF consent dialog returned through `IRunUi.ShowConsentDialog`.
+- Invariant: Confirm must not independently create a `RestorePlan`, pre-create a snapshot folder, close a process, evaluate a snapshot decision, or call a module's `Restore*` member. Only the orchestrator does those operations, in its existing order.
+
+- [ ] **Step 1: Write failing Confirm and dialog tests**
+
+```csharp
+[Fact]
+public void Confirm_GroupsOnlyExistingProcessAndExplorerImpacts()
+{
+ WpfTestHost.Run(() =>
+ {
+ ConfirmViewModel viewModel = new ConfirmViewModel(
+ SnapshotEventForTemporaryFolder(),
+ new[] { ModuleWithConsentProcess(), ModuleWithExplorerRestartAndWarning() });
+
+ Assert.Equal(RestorePlan.FidelityCaveat, viewModel.FidelityCaveat);
+ Assert.Contains(viewModel.ConsentProcesses, item => item.DisplayName == "Visual Studio Code");
+ Assert.Contains(viewModel.ExplorerRestartModules, item => item.Title == "Taskbar");
+ Assert.Contains(viewModel.ModuleWarnings, item => item.Text == "Existing sign-out warning.");
+ });
+}
+
+[Fact]
+public void RestoreConsentDialog_IsOwnerBoundAndDefaultsToNoConsentedProcesses()
+{
+ WpfTestHost.Run(() =>
+ {
+ Window owner = new Window();
+ RestoreConsentDialog dialog = RestoreConsentDialog.Create(owner, PlanWithOneConsentEntry());
+
+ Assert.Same(owner, dialog.Owner);
+ Assert.Empty(dialog.ConsentedProcessNames);
+ Assert.False(dialog.DialogResult == true);
+ dialog.Close();
+ owner.Close();
+ });
+}
+
+[Fact]
+public async Task Confirm_StartRestore_InvokesExistingOrchestratorWithSelectedSource()
+{
+ await WpfTestHost.RunAsync(async () =>
+ {
+ string source = CreateTemporarySnapshotFolder();
+ CancelingRunDialogService dialogs = new();
+ ConfirmViewModel viewModel = AttachedConfirm(SnapshotFor(source), new[] { new TestModule() }, dialogs);
+
+ await viewModel.StartRestoreAsync();
+
+ Assert.Equal(Path.GetFullPath(source), dialogs.LastRestorePlan.RestoreSourcePath);
+ Assert.Single(dialogs.LastRestorePlan.Modules);
+ Assert.Contains("canceled", viewModel.Summary.Headline, StringComparison.OrdinalIgnoreCase);
+ Assert.False(RunCoordinator.IsRunning);
+ });
+}
+```
+
+Make the Test dialog service record the supplied plan in `LastRestorePlan` and return `null` from `ShowRestoreConsent`. The real orchestrator then reaches its existing safe cancellation branch after composing a genuine `RestorePlan`, without writing or restoring anything. Add this asynchronous STA helper to the Foundation-created test host; it must pump the owning WPF dispatcher until the delegate finishes and rethrow its original exception:
+
+```csharp
+internal static Task RunAsync(Func action)
+{
+ TaskCompletionSource completion =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+ Thread thread = new(() =>
+ {
+ Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
+ dispatcher.BeginInvoke(new Action(async () =>
+ {
+ try
+ {
+ await action();
+ completion.TrySetResult(null);
+ }
+ catch (Exception ex)
+ {
+ completion.TrySetException(ex);
+ }
+ finally
+ {
+ dispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
+ }
+ }));
+ Dispatcher.Run();
+ });
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ return completion.Task;
+}
+```
+
+- [ ] **Step 2: Run the tests to verify Confirm and WPF consent do not yet exist**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ConfirmViewModelTests|FullyQualifiedName~RestoreConsentDialogTests"
+```
+
+Expected: compilation fails because the Confirm view model, WPF dialog, and owner-bound dialog service are undefined.
+
+- [ ] **Step 3: Implement impact grouping without inventing machine-state metadata**
+
+```csharp
+internal sealed class Notice
+{
+ internal Notice(string title, string text) { Title = title; Text = text; }
+ public string Title { get; }
+ public string Text { get; }
+}
+
+internal sealed class ConfirmViewModel : ObservableObject, IRunPresentation
+{
+ private Dispatcher dispatcher;
+ private IRunDialogService dialogService;
+ private Func ownerProvider;
+ private RunControl activeControl;
+ private bool isRestoring;
+ private readonly Action backToCompare;
+ private readonly ObservableCollection logLines = new();
+
+ internal ConfirmViewModel(SnapshotEvent snapshot, IReadOnlyList modules,
+ Action backToCompare = null)
+ {
+ Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
+ Modules = modules?.Where(module => module != null).ToArray() ?? Array.Empty();
+ this.backToCompare = backToCompare;
+ LogLines = new ReadOnlyObservableCollection(logLines);
+ FidelityCaveat = RestorePlan.FidelityCaveat;
+ ConsentProcesses = Modules.SelectMany(module => module.ProcessesToCloseBeforeRestore ?? Array.Empty())
+ .Where(requirement => requirement != null && requirement.NeedsConsent)
+ .GroupBy(requirement => requirement.ProcessName, StringComparer.OrdinalIgnoreCase)
+ .Select(group => group.First()).ToArray();
+ InformationalProcesses = Modules.SelectMany(module => module.ProcessesToCloseBeforeRestore ?? Array.Empty())
+ .Where(requirement => requirement != null && !requirement.NeedsConsent)
+ .GroupBy(requirement => requirement.ProcessName, StringComparer.OrdinalIgnoreCase)
+ .Select(group => group.First()).ToArray();
+ ExplorerRestartModules = Modules.Where(module => module.RequiresExplorerRestart).ToArray();
+ ModuleWarnings = Modules.Where(module => !string.IsNullOrWhiteSpace(module.WarningMessage))
+ .Select(module => new Notice(module.Title, module.WarningMessage)).ToArray();
+
+ StartRestoreCommand = new DelegateCommand(_ => _ = StartFromCommandAsync(), _ => CanStartRestore);
+ CancelRestoreCommand = new DelegateCommand(_ => activeControl?.RequestCancellation(), _ => IsRestoring);
+ BackToCompareCommand = new DelegateCommand(_ => this.backToCompare?.Invoke(), _ => CanNavigate);
+ }
+
+ public SnapshotEvent Snapshot { get; }
+ public IReadOnlyList Modules { get; }
+ public IReadOnlyList ConsentProcesses { get; }
+ public IReadOnlyList InformationalProcesses { get; }
+ public IReadOnlyList ExplorerRestartModules { get; }
+ public IReadOnlyList ModuleWarnings { get; }
+ public string FidelityCaveat { get; }
+ public bool IsSourcePartial => Snapshot.Kind == SnapshotEventKind.Partial;
+ public bool IsRestoring
+ {
+ get => isRestoring;
+ private set { isRestoring = value; OnPropertyChanged(); OnPropertyChanged(nameof(CanStartRestore)); OnPropertyChanged(nameof(CanNavigate)); StartRestoreCommand.RaiseCanExecuteChanged(); CancelRestoreCommand.RaiseCanExecuteChanged(); BackToCompareCommand.RaiseCanExecuteChanged(); }
+ }
+ public bool CanStartRestore => Modules.Count != 0 && !IsRestoring && dialogService != null && ownerProvider != null;
+ public bool CanNavigate => !IsRestoring;
+ public string ProgressText { get; private set; } = string.Empty;
+ public RunSummary Summary { get; private set; }
+ public DelegateCommand StartRestoreCommand { get; }
+ public DelegateCommand CancelRestoreCommand { get; }
+ public DelegateCommand BackToCompareCommand { get; }
+ public ReadOnlyObservableCollection LogLines { get; }
+
+ internal void AttachRunSurfaces(Dispatcher dispatcher, Func ownerProvider,
+ IRunDialogService dialogs)
+ {
+ this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+ this.ownerProvider = ownerProvider ?? throw new ArgumentNullException(nameof(ownerProvider));
+ dialogService = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
+ OnPropertyChanged(nameof(CanStartRestore));
+ StartRestoreCommand.RaiseCanExecuteChanged();
+ }
+
+ private async Task StartFromCommandAsync()
+ {
+ try { await StartRestoreAsync(); }
+ catch (Exception ex) { ProgressText = "Restore could not start: " + ex.Message; OnPropertyChanged(nameof(ProgressText)); }
+ }
+
+ private void AppendLog(string text) => logLines.Add(text ?? string.Empty);
+ private void ClearLog() => logLines.Clear();
+
+ public void SetProgressText(string text) { ProgressText = text ?? string.Empty; OnPropertyChanged(nameof(ProgressText)); }
+ public void SetProgressPercent(int percent) { }
+ public void SetProgressDetail(string groupInfo, string elapsed, string remaining, string throughput, long bytesWritten, int errors, int warnings) { }
+ public void ShowSummary(RunSummary summary, string caption, IReadOnlyList outcomes)
+ { Summary = summary; OnPropertyChanged(nameof(Summary)); }
+ public void SetExplorerRestartVisible(bool visible) { ExplorerRestartVisible = visible; OnPropertyChanged(nameof(ExplorerRestartVisible)); }
+ public bool ExplorerRestartVisible { get; private set; }
+
+}
+```
+
+Do not define a reboot collection, enum, label, or view-model property. Show a warning text only as the module supplied it, including an existing sign-out warning.
+
+- [ ] **Step 4: Implement owner-bound final dialogs and default-no snapshot override**
+
+```csharp
+internal sealed class RestoreRunDialogService : IRunDialogService
+{
+ private readonly Window owner;
+
+ internal RestoreRunDialogService(Window owner)
+ => this.owner = owner ?? throw new ArgumentNullException(nameof(owner));
+
+ public IReadOnlyList ShowRestoreConsent(RestorePlan plan)
+ {
+ RestoreConsentDialog dialog = RestoreConsentDialog.Create(owner, plan);
+ return dialog.ShowDialog() == true ? dialog.ConsentedProcessNames : null;
+ }
+
+ public bool ConfirmSnapshotOverride(string text, string caption)
+ => MessageBox.Show(owner, text, caption, MessageBoxButton.YesNo,
+ MessageBoxImage.Warning, MessageBoxResult.No) == MessageBoxResult.Yes;
+
+ public void ShowPlanCompositionError(string text, string caption)
+ => MessageBox.Show(owner, text, caption, MessageBoxButton.OK, MessageBoxImage.Error);
+}
+```
+
+```csharp
+internal sealed class ConsentChoice : ObservableObject
+{
+ internal ConsentChoice(RestoreConsentEntry entry) => Entry = entry;
+ internal RestoreConsentEntry Entry { get; }
+ public string Label => Entry.Label;
+ public bool IsSelected { get; set; }
+}
+
+internal sealed partial class RestoreConsentDialog : Window
+{
+ private readonly RestorePlan plan;
+
+ internal static RestoreConsentDialog Create(Window owner, RestorePlan plan)
+ => new RestoreConsentDialog(plan) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)) };
+
+ internal RestoreConsentDialog(RestorePlan plan)
+ {
+ this.plan = plan ?? throw new ArgumentNullException(nameof(plan));
+ ConsentChoices = new ObservableCollection(
+ this.plan.ConsentEntries.Select(entry => new ConsentChoice(entry)));
+ InitializeComponent();
+ DataContext = this;
+ }
+
+ public string ConfirmationText => plan.ConfirmationText;
+ public string FidelityCaveat => RestorePlan.FidelityCaveat;
+ public IReadOnlyList InformationalCloseLines => plan.InformationalCloseLines;
+ public ObservableCollection ConsentChoices { get; }
+ public IReadOnlyList ConsentedProcessNames
+ => ConsentChoices.Where(choice => choice.IsSelected)
+ .Select(choice => choice.Entry.ProcessName).ToArray();
+
+ private void Restore_Click(object sender, RoutedEventArgs e) => DialogResult = true;
+}
+```
+
+`RestoreConsentDialog.Create` must set `Owner = owner`; all `RestoreConsentEntry` checkboxes start unchecked; its Cancel button has `IsCancel="True"`; and its Restore button sets `DialogResult = true` only after explicit click. `ShowRestoreConsent` returns `null` for Escape, close, or Cancel, preserving the orchestrator's cancellation behavior. The final dialog renders the actual `RestorePlan.ConfirmationText`, `FidelityCaveat`, `ConsentEntries`, and `InformationalCloseLines` generated after the real pre-restore snapshot destination is chosen.
+
+- [ ] **Step 5: Implement WPF launch using the existing orchestration sequence**
+
+```csharp
+internal async Task StartRestoreAsync()
+{
+ if (Modules.Count == 0 || IsRestoring || dialogService == null || ownerProvider == null || !RunCoordinator.TryStart())
+ return;
+
+ activeControl = new RunControl();
+ bool logSinkInstalled = false;
+ try
+ {
+ WpfLogSink logSink = new(dispatcher, AppendLog, ClearLog);
+ WpfRunUi runUi = new(dispatcher, this, dialogService, ownerProvider);
+ LogHelper.Instance.SetSink(logSink);
+ logSinkInstalled = true;
+ IsRestoring = true;
+
+ await new BackupRestoreOrchestrator(runUi, activeControl)
+ .RunRestore(Modules, Snapshot.CanonicalPath);
+ }
+ catch (Exception ex)
+ {
+ ShowSummary(RunSummary.For(Array.Empty(), false, RunVerb.Restore, ex.Message),
+ "Restore", Array.Empty());
+ }
+ finally
+ {
+ if (logSinkInstalled)
+ LogHelper.Instance.SetSink(null);
+ activeControl?.Dispose();
+ activeControl = null;
+ IsRestoring = false;
+ RunCoordinator.SetRunning(false);
+ }
+}
+```
+
+Wire the visible Cancel action to `activeControl.RequestCancellation()` while `IsRestoring` is true. The method must pass the original selected snapshot directory, not an extracted comparison path, because `RunRestore` owns its own `BackupPayload.TryPrepareForRead` scope. Never call `SnapshotGate`, `RestoreScope.For`, `RestoreDispatch.Decide`, or `ExplorerRestartPrompt.IsNeeded` from WPF: the orchestrator invokes them in the retained safe order and reaches the owner-bound `ConfirmSnapshotOverride` exactly when `SnapshotDecision.RequiresOverride` is true.
+
+- [ ] **Step 6: Add accessible Confirm and consent XAML**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```csharp
+// ConfirmView.xaml.cs; invoked from Loaded after the view has a real main-window owner.
+private void ConfirmView_Loaded(object sender, RoutedEventArgs e)
+{
+ if (DataContext is not ConfirmViewModel viewModel)
+ return;
+
+ Window owner = Window.GetWindow(this)
+ ?? throw new InvalidOperationException("Confirm must be hosted in a Window.");
+ viewModel.AttachRunSurfaces(Dispatcher, () => Window.GetWindow(this),
+ new RestoreRunDialogService(owner));
+}
+```
+
+This code-behind performs only WPF owner composition. `RestoreRunDialogService` retains the owner for WPF modal dialogs, while Foundation's `WpfRunUi` invokes the supplied live `Func` and exposes its opaque result through `IRunUi.DialogOwner` to the retained Core `AppStoreApps.RestoreAsync(path, ui.DialogOwner)` / `RestoreDialog` callback seam; it contains no restore policy or data access.
+
+The Confirm view's primary button is intentionally not the final modal consent. Clicking it lets the orchestrator compose the exact plan and opens `RestoreConsentDialog`, where the user explicitly clicks Restore. This keeps the existing partial pre-restore-snapshot consent after `SnapshotGate` at its mandatory point immediately before writes.
+
+- [ ] **Step 7: Run the focused Confirm/dialog tests and verify the safe orchestrator path**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ConfirmViewModelTests|FullyQualifiedName~RestoreConsentDialogTests|FullyQualifiedName~RestoreConsentCancellationTests"
+```
+
+Expected: PASS; the final dialog is owned by the active WPF window with no preselected consent, only declared Core impacts are shown, and a final-dialog cancellation produces the existing no-changes restore summary through `BackupRestoreOrchestrator`.
+
+- [ ] **Step 8: Commit Confirm and owner-bound restore consent**
+
+```powershell
+git add src/WinRestoreKit.Wpf/ViewModels/ConfirmViewModel.cs src/WinRestoreKit.Wpf/Services/RestoreRunDialogService.cs src/WinRestoreKit.Wpf/Views/ConfirmView.xaml src/WinRestoreKit.Wpf/Views/ConfirmView.xaml.cs src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml src/WinRestoreKit.Wpf/Views/Dialogs/RestoreConsentDialog.xaml.cs src/WinRestoreKit.Wpf/Navigation/CompareWorkflowNavigator.cs src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs src/WinRestoreKit.Tests/WpfTestHost.cs src/WinRestoreKit.Tests/ConfirmViewModelTests.cs src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs
+git commit -m "feat: confirm WPF restores safely"
+```
+
+### Task 6: Verify the complete Compare → Confirm path without changing backup or cutover scope
+
+**Files:**
+- Modify: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ConfirmViewModelTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs`
+
+**Interfaces:**
+- Consumes: all contracts from Tasks 1–5.
+- Produces: a tested Stage-3 workflow boundary. No project identity, publish configuration, backup creation flow, WinForms deletion, or semantic-provider surface is produced by this task.
+
+- [ ] **Step 1: Add failing cross-workflow regression tests for snapshot replacement and partial source labeling**
+
+```csharp
+[Fact]
+public async Task Navigator_ChangingSnapshotWithRestoreSet_CancelKeepsOriginalSetAndDisposesIncomingScope()
+{
+ await WpfTestHost.RunAsync(async () =>
+ {
+ TestDiscardDialog dialogs = new(answer: false);
+ CompareWorkflowNavigator navigator = Navigator(dialogs);
+ navigator.OpenCompare(Prepared("first"));
+ await navigator.PendingTransition;
+ navigator.CurrentWorkspace.RestoreSet.Add(navigator.CurrentWorkspace.Rows[0].Comparison);
+
+ string incomingOwnedPath = Path.Combine(Path.GetTempPath(), "WinRestoreKit.Tests",
+ Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(incomingOwnedPath);
+ SnapshotPayloadPreparation incoming = new SnapshotPayloadPreparation(
+ SnapshotFor(incomingOwnedPath, "second"),
+ new BackupPayload.ReadScope(incomingOwnedPath, incomingOwnedPath), error: null);
+
+ navigator.OpenCompare(incoming);
+ await navigator.PendingTransition;
+
+ Assert.Equal("first", navigator.CurrentWorkspace.Snapshot.DisplayName);
+ Assert.True(navigator.CurrentWorkspace.RestoreSet.HasItems);
+ Assert.False(Directory.Exists(incomingOwnedPath));
+ });
+}
+
+[Fact]
+public void Confirm_PartialSourceStaysExplicitWithoutBypassingSnapshotGate()
+{
+ WpfTestHost.Run(() =>
+ {
+ ConfirmViewModel viewModel = new ConfirmViewModel(PartialSnapshot(), new[] { ModuleWithArtifact() });
+
+ Assert.Equal(SnapshotEventKind.Partial, viewModel.Snapshot.Kind);
+ Assert.True(viewModel.IsSourcePartial);
+ });
+}
+```
+
+The second test pins the separation between a selected partial source and the later, orchestrator-owned pre-restore `SnapshotGate` result: WPF can label the source honestly but cannot pre-authorize an override.
+
+- [ ] **Step 2: Run the Stage-3 focused test set and verify the new integration tests fail**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests|FullyQualifiedName~RestoreSetViewModelTests|FullyQualifiedName~ComparisonWorkspaceViewModelTests|FullyQualifiedName~ConfirmViewModelTests|FullyQualifiedName~RestoreConsentDialogTests"
+```
+
+Expected: FAIL until snapshot replacement retains/cancels state exactly as specified and Confirm exposes the partial-source label while leaving the snapshot override decision to the orchestrator.
+
+- [ ] **Step 3: Implement the remaining workflow facts and preserve existing tests**
+
+```csharp
+internal bool IsSourcePartial => Snapshot.Kind == SnapshotEventKind.Partial;
+```
+
+Complete `CompareWorkflowNavigator` so it disposes every rejected incoming preparation, waits for `CancelAsync()` before replacing an active comparison, and clears the old restore set only after accepted replacement. Keep the existing WinForms-only regression tests unchanged while the side-by-side shell remains runnable; later cutover removes them only after explicit WPF/Application replacements. Do not delete them in this plan.
+
+- [ ] **Step 4: Run all Stage-3 focused tests and verify success**
+
+Run:
+
+```powershell
+dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotComparisonServiceTests|FullyQualifiedName~RestoreSetViewModelTests|FullyQualifiedName~ComparisonWorkspaceViewModelTests|FullyQualifiedName~ConfirmViewModelTests|FullyQualifiedName~RestoreConsentDialogTests|FullyQualifiedName~RestoreContentsTests|FullyQualifiedName~RestorePlanTests|FullyQualifiedName~RestoreScopeTests|FullyQualifiedName~RestoreDispatchTests|FullyQualifiedName~SnapshotGateConsentTests|FullyQualifiedName~ExplorerRestartPromptTests"
+```
+
+Expected: PASS; comparison behavior is deterministic and isolated, WPF selection/confirmation is safe, and the retained Core restore gates still pass their original contracts.
+
+- [ ] **Step 5: Build and run the WPF Compare/Confirm smoke path**
+
+Run:
+
+```powershell
+dotnet build src/WinRestoreKit.sln -c Debug
+dotnet run --project src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj -c Debug
+```
+
+Expected: build succeeds with zero errors; the WPF app opens while the WinForms project remains runnable.
+
+In the launched WPF application, exercise this exact read-only path with a local verified or partial Timeline snapshot:
+
+1. Select the snapshot with keyboard Enter; confirm its Compare view names the selected snapshot and starts with **All modules** selected.
+2. Wait for results; verify Changed, Same, Unavailable, and Not captured labels remain textually distinct, and a known unavailable row remains visible under All.
+3. Switch to Changed only, then back to All; verify row evidence and any already-added usable module remain unchanged.
+4. Open a row's detail tray; verify it displays only target/process declarations, existing warning text, and an Explorer note only for modules declaring `RequiresExplorerRestart`; verify no reboot claim is displayed.
+5. Add a Changed row and, if available, an Unavailable row with a proven artifact; verify a Not captured row cannot be added. Select another Timeline snapshot, first choose Cancel and confirm the original restore set remains, then choose the explicit discard action and confirm the set clears.
+6. Continue to Confirm; verify selected modules and process/Explorer groups, then press Continue to restore. Verify the modal consent dialog is centered on and blocked by the main WPF window, has no prechecked consent boxes, and Cancel returns the existing no-changes result without a restore write.
+7. For a controlled test where the pre-restore snapshot is incomplete, verify the owner-bound `Pre-restore snapshot` Yes/No prompt appears only after the original pipeline reaches `SnapshotGate`, defaults to No, and No prevents restore writes.
+
+- [ ] **Step 6: Commit the verified Stage-3 boundary**
+
+```powershell
+git add src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs src/WinRestoreKit.Tests/ConfirmViewModelTests.cs src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs
+git commit -m "test: cover compare confirm restore workflow"
+```
+
+## Plan Self-Review
+
+- **Spec coverage:** Tasks 2–3 cover arbitrary selected snapshot comparison, manifest precedence, the four states, per-module isolation, bounded cancellation, and read-scope cleanup. Task 4 covers All by default, Changed only, ordered rows, detail tray, whole-module selection, and snapshot-change clearing. Task 5 preserves restore impacts, creates owner-bound consent, and launches only the existing orchestration/gates. Task 6 exercises the combined selection/confirmation and WPF smoke path. Backup creation, progress/results migration, app-restoration UX expansion, project identity, publishing, and cutover are intentionally delegated to their respective later plans.
+- **No fabricated evidence:** No XAML or ViewModel parses registry exports/payload contents, constructs before/after values, adds semantic providers, or creates reboot metadata. All visible impacts come from existing typed Core declarations or verbatim module warnings.
+- **Type consistency:** Every later WPF component consumes `BackupModuleRegistration`, `SnapshotEvent`, `SnapshotPayloadPreparation`, `ModuleComparison`, `RestoreSetViewModel`, and the Foundation `WpfRunUi` interfaces defined in the interface map. The required public comparison signature remains exactly `CompareAsync(SnapshotEvent, IReadOnlyList, CancellationToken)`.
+- **Placeholder scan:** This plan contains no TODO/TBD/future implementation steps. Deferred scope is named only where the approved migration sequence assigns it to a different complete plan.
+
diff --git a/docs/superpowers/plans/2026-08-09-timeline-compare-wpf-migration-roadmap.md b/docs/superpowers/plans/2026-08-09-timeline-compare-wpf-migration-roadmap.md
new file mode 100644
index 0000000..11fa4fa
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-timeline-compare-wpf-migration-roadmap.md
@@ -0,0 +1,77 @@
+# Timeline + Compare WPF Migration Roadmap
+
+> **For agentic workers:** Execute the linked implementation plans in order. Each linked plan requires `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` and contains its own checkbox steps, tests, review gate, and commits.
+
+**Goal:** Replace WinRestoreKit’s WinForms shell with the approved Timeline + Compare WPF application while preserving Core behavior, restore safeguards, snapshot compatibility, and the self-contained Windows executable.
+
+**Architecture:** Introduce a framework-neutral `WinRestoreKit.Application` layer between `WinRestoreKit.Core` and the two temporary shells. Build and verify the WPF shell side by side, migrate one complete workflow at a time, then remove WinForms only after parity and real-desktop verification.
+
+**Tech Stack:** .NET 8, C#, WPF, MVVM, xUnit, Windows UI Automation, self-contained `win-x64` single-file publishing.
+
+## Global Constraints
+
+- Preserve existing backup and restore engine semantics, snapshot format, local storage model, and safety gates.
+- Keep the WinForms application runnable until Timeline, Compare/Confirm, Backup/Progress, History, Settings, About, and dialogs have verified WPF equivalents.
+- `WinRestoreKit.Application` must reference neither WinForms nor WPF.
+- WPF views and view models must not parse registry exports or invent comparison values.
+- Comparison is read-only and uses `HasArtifactIn` followed by `HasDriftedFrom`; one module failure must not erase other results.
+- Restore selection remains whole-module and incomplete-snapshot consent remains mandatory immediately before restore writes.
+- Do not retain unsafe incomplete folders merely to make failed attempts durable; non-persistable failures are current-session events only.
+- Support Follow system, Light, and Dark themes, 100–200% DPI, a 1024 px minimum usable width, reduced motion, keyboard operation, and UI Automation.
+- Preserve `highestAvailable`, `longPathAware`, `WinRestoreKit.ico`, the exact `Properties/AssemblyInfo.cs` version source, and `GenerateAssemblyInfo=false` on the shipping app.
+- Final publishing remains self-contained `win-x64`, single-file, native self-extracting, compressed, and untrimmed.
+- Final cutover removes obsolete WinForms views, forms, controls, resources, helpers, tests, and compatibility paths; no shims remain.
+
+---
+
+## Execution Order
+
+- [ ] **Stage 1: Build the neutral application layer and side-by-side WPF shell**
+
+ Execute: [`2026-08-09-wpf-foundation-application-shell.md`](2026-08-09-wpf-foundation-application-shell.md)
+
+ Gate: both the existing WinForms application and the new WPF shell build and launch; shared orchestration compiles without a UI-framework reference; theme/settings/about/update seams have focused tests.
+
+- [ ] **Stage 2: Replace Home and primary History with the Timeline event model**
+
+ Execute: [`2026-08-09-timeline-event-model.md`](2026-08-09-timeline-event-model.md)
+
+ Gate: Timeline and advanced History consume one event source; ordering and event states are deterministic; verified/partial snapshots are selectable; failed/unreadable events are diagnostic-only; compressed selection cleans up its private read scope.
+
+- [ ] **Stage 3: Add honest comparison, restore selection, and confirmation**
+
+ Execute: [`2026-08-09-compare-confirm-restore.md`](2026-08-09-compare-confirm-restore.md)
+
+ Gate: arbitrary selected snapshots produce Changed, Same, Unavailable, and Not captured module rows; the default All-modules filter cannot hide unsupported probes; restore selection and consent use existing safety contracts; comparison remains read-only.
+
+- [ ] **Stage 4: Migrate Create Snapshot, progress, results, and app restore**
+
+ Execute: [`2026-08-09-backup-progress-results.md`](2026-08-09-backup-progress-results.md)
+
+ Gate: Create snapshot → selection → progress → result → Timeline works in WPF; run admission, pause, cancel, compression, manifest/log, late-cancel wording, dialogs, and session-event behavior match current contracts.
+
+- [ ] **Stage 5: Verify parity and perform the clean WPF cutover**
+
+ Execute: [`2026-08-09-wpf-cutover-release-verification.md`](2026-08-09-wpf-cutover-release-verification.md)
+
+ Gate: real Windows desktop verification passes before deletion; WPF is the sole shipping `WinRestoreKit` application; relevant tests pass; all obsolete WinForms artifacts are gone; the publish directory contains exactly one verified `WinRestoreKit.exe`.
+
+## Cross-Stage Review Rules
+
+1. Run each linked plan’s focused test command before its commit.
+2. Run `dotnet build src/WinRestoreKit.sln -c Debug` at every stage gate.
+3. Run `dotnet test src/WinRestoreKit.sln -c Debug --no-build` after the corresponding successful build.
+4. Review each stage against `docs/superpowers/specs/2026-08-09-timeline-compare-wpf-shell-design.md` before starting the next stage.
+5. Stop the cutover if a workflow exists only in WinForms, a comparison value lacks Core evidence, a restore gate can be bypassed, or the WPF executable has not passed real-desktop verification.
+
+## Completion Evidence
+
+The migration is complete only when the Stage 5 plan records:
+
+- the full build and test outputs;
+- the runtime smoke-test matrix for Timeline, Compare, Confirm, Backup, Progress, Results, History, Settings, About, and dialogs;
+- keyboard, UI Automation, reduced-motion, theme, responsive-width, and DPI results;
+- screenshot-baseline comparisons;
+- the exact publish command and one-file directory listing;
+- executable metadata, checksum, launch evidence, manifest/elevation behavior, packaged resource behavior, and update-version coherence; and
+- a repository search showing no obsolete WinForms project, source, designer, resource, control, helper, or compatibility path remains.
diff --git a/docs/superpowers/plans/2026-08-09-timeline-event-model.md b/docs/superpowers/plans/2026-08-09-timeline-event-model.md
new file mode 100644
index 0000000..5e1a5ca
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-timeline-event-model.md
@@ -0,0 +1,906 @@
+# Timeline Event Model Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Deliver one framework-neutral snapshot-event source and an accessible WPF Timeline plus advanced History that can select verified or partial snapshots safely, while making failures and unreadable entries diagnostic-only.
+
+**Architecture:** Move filesystem discovery out of the WinForms view layer into `WinRestoreKit.Application`, preserving the existing root-recognition and manifest/payload semantics. `SnapshotEventCatalog` classifies and orders immutable events once; both WPF representations project the same event object through the same status projection. A selected event is prepared by a disposable Application-layer payload service and ownership transfers to the future Compare stage without exposing payload or manifest text to XAML.
+
+**Tech Stack:** .NET 8 (`net8.0-windows` framework-neutral Application layer and `net8.0-windows` WPF/test projects), WPF/MVVM, xUnit 2.9.3, Core `BackupManifest`, `BackupPayload`, `BackupLog`, `BackupRootRegistry`, and `DataHelper.Data`.
+
+## Global Constraints
+
+- Execute this plan after the Foundation plan has created `src/WinRestoreKit.Application/WinRestoreKit.Application.csproj`, moved `Settings/BackupRootRegistry.cs`, and granted Application access to Core internals.
+- `WinRestoreKit.Application` MUST reference neither WinForms nor WPF; its namespace remains `WinRestoreKit`.
+- Preserve the existing Core snapshot format, backup/restore behavior, custom-root recognition, and `BackupPayload.TryPrepareForRead` cleanup semantics.
+- The shared public event contract is exactly `SnapshotEventKind { Verified, Partial, Failed, Unreadable }`, immutable `SnapshotEvent`, and `SnapshotEventCatalog.Read(...)` (implemented here as the public instance method `Read()`).
+- The sort is deterministic: descending event timestamp, then `StringComparer.Ordinal` on the canonical full path. Never rely on directory enumeration order or localized string comparison.
+- `Verified` and `Partial` are selectable; `Failed` and `Unreadable` are diagnostic-only and must never enter Compare or Restore.
+- Durable failures are limited to retained attempts: a folder which remains on disk and has retained folder/manifest/log evidence survives restart and is rediscovered. Folder-creation failures and cancelled runs whose owned folder was deleted by cleanup are session-only events; they MUST NOT be persisted and MUST NOT affect retention or cleanup safety.
+- A failed custom root or manifest/payload read must show the real exception message or the truthful validation failure; no error may be rendered as “No backups yet”, an empty snapshot, or a guessed success.
+- Compressed selection uses a private `BackupPayload.ReadScope`; every unsuccessful path disposes it immediately, and the receiver that accepts a successful preparation owns disposal.
+- WPF uses MVVM. Views never parse registry exports, manifests, logs, or payload files. This plan intentionally does not create comparison rows or restore confirmation.
+- Keep the existing WinForms shell runnable during migration. Do not add a compatibility overload or a second status-classification path.
+- Keep tests in `src/WinRestoreKit.Tests`; update their project references for Application/WPF and add an STA helper for WPF construction.
+
+## Produced Interfaces
+
+These are the only Timeline interfaces later plans may consume.
+
+```csharp
+namespace WinRestoreKit;
+
+public enum SnapshotEventKind { Verified, Partial, Failed, Unreadable }
+
+public interface ISnapshotEventReader
+{
+ IReadOnlyList Read();
+}
+
+public sealed class SnapshotEvent
+{
+ internal SnapshotEvent(
+ SnapshotEventKind kind, DateTime created, string displayName,
+ string canonicalPath, string diagnosticReason, string machineName,
+ long sizeBytes, bool isSizeComplete, ManifestData manifest);
+
+ public SnapshotEventKind Kind { get; }
+ public DateTime Created { get; }
+ public string DisplayName { get; }
+ public string CanonicalPath { get; }
+ public string DiagnosticReason { get; }
+ public string MachineName { get; }
+ public long SizeBytes { get; }
+ public bool IsSizeComplete { get; }
+ public bool IsRestorable { get; } // true only for Verified and Partial
+
+ // Internal so WPF never sees Core persistence text. Application comparison code may read it.
+ internal ManifestData Manifest { get; }
+}
+
+public sealed class SnapshotEventCatalog : ISnapshotEventReader
+{
+ public SnapshotEventCatalog();
+ public IReadOnlyList Read();
+ public void RecordSessionFailure(DateTime created, string displayName, string diagnosticReason);
+}
+
+public interface ISnapshotPayloadPreparationService
+{
+ Task PrepareAsync(
+ SnapshotEvent snapshot, CancellationToken cancellationToken);
+}
+
+public sealed class SnapshotPayloadPreparation : IDisposable
+{
+ internal SnapshotPayloadPreparation(SnapshotEvent snapshot, BackupPayload.ReadScope scope, string error);
+ public SnapshotEvent Snapshot { get; }
+ public string Path { get; }
+ public string Error { get; }
+ public bool IsPrepared { get; }
+ public void Dispose();
+}
+
+public sealed class SnapshotPayloadPreparationService : ISnapshotPayloadPreparationService
+{
+ public Task PrepareAsync(
+ SnapshotEvent snapshot, CancellationToken cancellationToken);
+}
+```
+
+```csharp
+// src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs
+namespace WinRestoreKit.Wpf.Navigation;
+
+internal interface ITimelineNavigator
+{
+ void OpenCompare(SnapshotPayloadPreparation preparation); // ownership transfers to receiver
+ void ShowSnapshotDiagnostic(SnapshotEvent snapshot);
+}
+```
+
+```csharp
+// Bound WPF types and every property XAML binds MUST be public: WPF binding reflects public
+// properties and silently drops inaccessible bindings.
+public sealed class SnapshotEventStatus
+{
+ public string Label { get; }
+ public string Glyph { get; }
+ public bool IsDiagnosticOnly { get; }
+}
+
+public sealed class SnapshotEventViewModel
+{
+ public SnapshotEvent Event { get; }
+ public SnapshotEventStatus Status { get; }
+ public string DisplayName { get; }
+ public string CreatedDisplay { get; }
+ public string DiagnosticReason { get; }
+ public bool HasDiagnostic { get; }
+ public string AutomationName { get; }
+}
+
+public sealed class TimelineViewModel : INotifyPropertyChanged
+{
+ internal TimelineViewModel(
+ ISnapshotEventReader catalog,
+ ISnapshotPayloadPreparationService preparationService,
+ ITimelineNavigator navigator);
+
+ public ReadOnlyObservableCollection Events { get; }
+ public SnapshotEventViewModel SelectedEvent { get; set; }
+ public string SelectionError { get; }
+ public bool HasSelectionError { get; }
+ public event PropertyChangedEventHandler PropertyChanged;
+ internal Task RefreshAsync(CancellationToken cancellationToken = default);
+ internal Task OpenSelectedAsync(CancellationToken cancellationToken = default);
+}
+
+public sealed class AdvancedHistoryViewModel : INotifyPropertyChanged
+{
+ internal AdvancedHistoryViewModel(ISnapshotEventReader catalog);
+
+ public ICollectionView Events { get; }
+ public string SearchText { get; set; }
+ public SnapshotEventViewModel SelectedEvent { get; set; }
+ public event PropertyChangedEventHandler PropertyChanged;
+ internal Task RefreshAsync(CancellationToken cancellationToken = default);
+}
+```
+
+---
+
+### Task 1: Move lossless backup-folder discovery into Application
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Snapshots/BackupFolders.cs`
+- Delete: `src/WinRestoreKit/Views/BackupFolders.cs`
+- Modify: `src/WinRestoreKit/WinRestoreKit.csproj`
+- Modify: `src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj`
+- Modify: `src/WinRestoreKit.Tests/BackupFoldersReadTests.cs`
+- Modify: `src/WinRestoreKit/MainForm.cs`
+- Modify: `src/WinRestoreKit/Views/HomePageView.cs`
+- Modify: `src/WinRestoreKit/Views/HistoryPageView.cs`
+- Modify: `src/WinRestoreKit/Views/ProgressPageView.cs`
+- Modify: `src/WinRestoreKit/Views/RestoreWizardStep1View.cs`
+- Modify: `src/WinRestoreKit/Views/RestoreWizardStep2View.cs`
+- Test: `src/WinRestoreKit.Tests/BackupFoldersReadTests.cs`
+
+**Interfaces:**
+- Consumes: Foundation’s framework-neutral Application project, `Settings/BackupRootRegistry.cs`, Core `Data.DataRootDir`, `BackupManifest`, `BackupPayload`, and `BackupLog`.
+- Produces: internal `WinRestoreKit.BackupFolders` and `BackupFolder` with the existing `Read()`, `Backups`, `Snapshots`, `UnreadableReason`, `Path`, `Name`, `DisplayName`, `Created`, and `ReadManifest()` members, plus internal `ManifestError` that records an invalid/unreadable manifest reason. Existing WinForms callers consume these through an Application friend assembly, not a copied View implementation.
+
+- [ ] **Step 1: Write failing discovery-preservation tests**
+
+Move the existing `BackupFoldersReadTests` import from `Views` to `WinRestoreKit`, retaining its custom-root coverage. Add a malformed-manifest test and an unreadable-root assertion that tests the actual message rather than a synthetic empty-state label:
+
+```csharp
+[Fact]
+public void Read_MalformedManifestKeepsValidationReason()
+{
+ RunWithRoots((defaultRoot, customRoot) =>
+ {
+ string folder = Directory.CreateDirectory(Path.Combine(defaultRoot, "bad-manifest")).FullName;
+ File.WriteAllText(Path.Combine(folder, BackupManifest.FileName), "{ not json");
+
+ BackupFolder found = Assert.Single(BackupFolders.Read().Backups);
+
+ Assert.Null(found.ReadManifest());
+ Assert.Equal("The backup manifest is invalid or uses an unsupported schema.", found.ManifestError);
+ });
+}
+```
+
+- [ ] **Step 2: Run the focused test and verify it fails**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~BackupFoldersReadTests`
+
+Expected: FAIL to compile because `WinRestoreKit.BackupFolders` and `BackupFolder.ManifestError` do not yet exist in Application.
+
+- [ ] **Step 3: Move the filesystem-only types and preserve root behavior**
+
+Move the old implementation without UI references to `src/WinRestoreKit.Application/Snapshots/BackupFolders.cs`, change its namespace to `WinRestoreKit`, and retain the existing default-root/custom-root rules verbatim: direct `Directory.GetDirectories`, recognizability only for custom children, nested-custom-root exclusion, snapshot-name handling, manifest timestamp preference, and legacy timestamp-name recognition. Do not retain a forwarding class in `Views`.
+
+Add explicit manifest evidence while retaining `ReadManifest()` compatibility for legacy WinForms consumers:
+
+```csharp
+internal sealed class BackupFolder
+{
+ private readonly ManifestData manifest;
+
+ internal BackupFolder(string path)
+ {
+ Path = path;
+ Name = System.IO.Path.GetFileName(path);
+ manifest = ReadManifest(path, out string manifestError);
+ ManifestError = manifestError;
+ Created = ReadCreated(path, manifest);
+ }
+
+ internal string ManifestError { get; }
+ internal ManifestData ReadManifest() => manifest;
+
+ private static ManifestData ReadManifest(string path, out string error)
+ {
+ error = null;
+ string file = System.IO.Path.Combine(path, BackupManifest.FileName);
+ try
+ {
+ if (!File.Exists(file))
+ return null;
+
+ ManifestData parsed = BackupManifest.TryParse(File.ReadAllText(file));
+ if (parsed == null)
+ error = "The backup manifest is invalid or uses an unsupported schema.";
+ return parsed;
+ }
+ catch (Exception ex)
+ {
+ error = ex.Message;
+ return null;
+ }
+ }
+}
+```
+
+Reference Application from the still-runnable WinForms project and tests; remove the compiled View source rather than leaving a duplicate filesystem reader:
+
+```xml
+
+
+
+
+```
+
+Foundation owns the necessary Application friend declaration; before moving the types, confirm `src/WinRestoreKit.Application/Properties/AssemblyInfo.cs` contains `[assembly: InternalsVisibleTo("WinRestoreKit")]`. Do not add a second declaration in this plan.
+
+Add `using WinRestoreKit;` to each of `MainForm.cs`, `HomePageView.cs`, `HistoryPageView.cs`, `ProgressPageView.cs`, `RestoreWizardStep1View.cs`, and `RestoreWizardStep2View.cs` when it is not already present. Preserve each existing call to `BackupFolders.Read()`, `BackupFolder`, `ReadManifest()`, and `IsSnapshot`; only its assembly owner changes. This is required to keep every WinForms backup/restore picker, progress surface, and existing Home/History view compiling and runnable while WPF is introduced.
+
+- [ ] **Step 4: Run the focused discovery tests and compile the side-by-side shell**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~BackupFoldersReadTests`
+
+Expected: PASS; existing default/custom/nested root cases still pass, and malformed JSON produces the validation reason while an inaccessible root retains its OS-supplied error message.
+
+Run: `dotnet build src/WinRestoreKit.sln`
+
+Expected: Build succeeds. The WinForms app still resolves `BackupFolders` and `BackupFolder` from its Application friend assembly; no `Views.BackupFolders` copy remains.
+
+- [ ] **Step 5: Commit the discovery extraction**
+
+```bash
+git add src/WinRestoreKit.Application/Snapshots/BackupFolders.cs src/WinRestoreKit/WinRestoreKit.csproj src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj src/WinRestoreKit/MainForm.cs src/WinRestoreKit/Views/HomePageView.cs src/WinRestoreKit/Views/HistoryPageView.cs src/WinRestoreKit/Views/ProgressPageView.cs src/WinRestoreKit/Views/RestoreWizardStep1View.cs src/WinRestoreKit/Views/RestoreWizardStep2View.cs src/WinRestoreKit.Tests/BackupFoldersReadTests.cs
+git rm src/WinRestoreKit/Views/BackupFolders.cs
+git commit -m "refactor: move backup folder discovery to application"
+```
+
+### Task 2: Add the immutable event DTO, status classifier, and deterministic catalog
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Snapshots/SnapshotEventKind.cs`
+- Create: `src/WinRestoreKit.Application/Snapshots/SnapshotEvent.cs`
+- Create: `src/WinRestoreKit.Application/Snapshots/SnapshotEventCatalog.cs`
+- Create: `src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs`
+
+**Interfaces:**
+- Consumes: Task 1’s lossless `BackupFolders`/`BackupFolder` discovery, `ManifestData`, manifest state literals, and moved `BackupRootRegistry` root configuration.
+- Produces: the exact public `SnapshotEventKind`, `SnapshotEvent`, and `SnapshotEventCatalog` interfaces in **Produced Interfaces**. Later comparison code may read `SnapshotEvent.Manifest` only inside Application; WPF consumes only public DTO fields.
+
+- [ ] **Step 1: Write failing catalog contract tests**
+
+Create a catalog test fixture that temporarily sets `Data.DataRootDir`, isolates `BackupRootRegistry`, and constructs one app-lifetime `SnapshotEventCatalog`. Write valid v1 manifests using the real schema fields. Add these focused facts:
+
+```csharp
+[Fact]
+public void Read_OrdersSameTimestampByOrdinalCanonicalPath()
+{
+ using TempDirectory root = TempDirectory.Create();
+ WriteManifest(root.Create("zulu"), "2026-08-09T12:00:00.0000000Z", "succeeded");
+ WriteManifest(root.Create("alpha"), "2026-08-09T12:00:00.0000000Z", "succeeded");
+ UseBackupRoots(root.Path, () =>
+ {
+ IReadOnlyList events = new SnapshotEventCatalog().Read();
+
+ Assert.Equal(new[] { "alpha", "zulu" }, events.Select(e => e.DisplayName));
+ });
+}
+
+[Fact]
+public void Read_ClassifiesFailedPartialAndUnreadableWithoutMakingThemRestorable()
+{
+ using TempDirectory root = TempDirectory.Create();
+ WriteManifest(root.Create("verified"), "2026-08-09T11:00:00.0000000Z", "succeeded");
+ WriteManifest(root.Create("partial"), "2026-08-09T10:00:00.0000000Z", "skipped");
+ WriteManifest(root.Create("failed"), "2026-08-09T09:00:00.0000000Z", "failed");
+ File.WriteAllText(Path.Combine(root.Create("broken"), BackupManifest.FileName), "not-json");
+
+ UseBackupRoots(root.Path, () =>
+ {
+ SnapshotEvent[] events = new SnapshotEventCatalog().Read().ToArray();
+
+ Assert.Equal(SnapshotEventKind.Verified, events.Single(e => e.DisplayName == "verified").Kind);
+ Assert.Equal(SnapshotEventKind.Partial, events.Single(e => e.DisplayName == "partial").Kind);
+ Assert.Equal(SnapshotEventKind.Failed, events.Single(e => e.DisplayName == "failed").Kind);
+ SnapshotEvent unreadable = events.Single(e => e.DisplayName == "broken");
+ Assert.Equal(SnapshotEventKind.Unreadable, unreadable.Kind);
+ Assert.False(events.Single(e => e.DisplayName == "failed").IsRestorable);
+ Assert.False(unreadable.IsRestorable);
+ Assert.NotEmpty(unreadable.DiagnosticReason);
+ });
+}
+```
+
+Also add facts for: an empty manifest module list is `Failed`; a manifest with any succeeded plus skipped/failed/unknown module is `Partial`; a missing manifest in a recognized legacy folder is `Partial`; an unreadable root produces one `Unreadable` event containing `ex.Message` while readable roots remain represented; and a `RecordSessionFailure` entry disappears when a fresh catalog instance simulates restart.
+
+- [ ] **Step 2: Run the catalog tests and verify they fail**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~SnapshotEventCatalogTests`
+
+Expected: FAIL to compile because `SnapshotEventKind`, `SnapshotEvent`, and `SnapshotEventCatalog` are absent.
+
+- [ ] **Step 3: Implement the one canonical status path**
+
+Keep all classification in `SnapshotEventCatalog`; do not reproduce `HistoryPageView.ReadResult`, `HomePageView.IsEntirelyFailed`, or text-log parsing. Construct a normal folder event with its canonical full path and actual size evidence. Classify only from trusted manifest evidence:
+
+```csharp
+private static SnapshotEventKind Classify(BackupFolder folder, ManifestData manifest)
+{
+ if (folder.ManifestError != null)
+ return SnapshotEventKind.Unreadable;
+
+ if (manifest == null)
+ return SnapshotEventKind.Partial; // legacy/manifest-silent evidence, never inferred verified
+
+ if (manifest.Modules.Count == 0 || manifest.Modules.All(m => m.State == BackupManifest.StateFailed))
+ return SnapshotEventKind.Failed;
+
+ if (manifest.Modules.Any(m => m.State != BackupManifest.StateSucceeded))
+ return SnapshotEventKind.Partial;
+
+ return SnapshotEventKind.Verified;
+}
+
+private static int CompareEvents(SnapshotEvent left, SnapshotEvent right)
+{
+ int timestamp = right.Created.CompareTo(left.Created);
+ return timestamp != 0
+ ? timestamp
+ : StringComparer.Ordinal.Compare(left.CanonicalPath, right.CanonicalPath);
+}
+```
+
+For each root enumeration failure, make an `Unreadable` event with `DiagnosticReason = ex.Message` and a canonicalized root path when possible. For invalid manifest content use Task 1’s validation reason; for an invalid directory timestamp use `DateTime.MinValue` and the real read error. Compute `SizeBytes` by enumerating recursively; set `IsSizeComplete = false` and retain the exception message as diagnostic evidence if an individual entry cannot be measured.
+
+Keep a private, in-memory `List` for `RecordSessionFailure`. Reject null/whitespace reasons with `ArgumentException`, generate a process-local `session://failure/` canonical path, classify it as `Failed`, and merge it only in that catalog instance’s `Read()`. It writes no folder, manifest, log, registry value, or retention marker. A retained directory with manifest/log/payload evidence is never recorded this way; it is rediscovered from disk so normal cleanup continues to be based on the existing durable storage rules.
+
+- [ ] **Step 4: Run the catalog tests and verify they pass**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~SnapshotEventCatalogTests`
+
+Expected: PASS; status, non-restorability, actual error diagnostics, stable ordinal canonical-path tie-breaking, and session-only failure lifetime all pass.
+
+- [ ] **Step 5: Commit the event source**
+
+```bash
+git add src/WinRestoreKit.Application/Snapshots/SnapshotEventKind.cs src/WinRestoreKit.Application/Snapshots/SnapshotEvent.cs src/WinRestoreKit.Application/Snapshots/SnapshotEventCatalog.cs src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs
+git commit -m "feat: add deterministic snapshot event catalog"
+```
+
+### Task 3: Prepare selected payloads with explicit ownership and cleanup
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparation.cs`
+- Create: `src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparationService.cs`
+- Create: `src/WinRestoreKit.Tests/SnapshotPayloadPreparationServiceTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotPayloadPreparationServiceTests.cs`
+
+**Interfaces:**
+- Consumes: Task 2 `SnapshotEvent`; Core `BackupPayload.TryPrepareForRead`, `BackupPayload.ReadScope`, and its archive validation/error behavior.
+- Produces: `SnapshotPayloadPreparation` and `SnapshotPayloadPreparationService.PrepareAsync(SnapshotEvent, CancellationToken)` exactly as declared above. A later Compare stage receives the successful preparation and owns its `Dispose()` call.
+
+- [ ] **Step 1: Write failing preparation tests against real loose and compressed folders**
+
+Use the existing `SnapshotCompressionTests`/`BackupPayloadTests` archive helpers rather than a fake zip reader. Cover successful compressed extraction, corrupted archive evidence, cancellation before extraction, failed-event rejection, and cleanup:
+
+```csharp
+[Fact]
+public async Task PrepareAsync_CompressedSnapshotDeletesPrivateExtractionWhenDisposed()
+{
+ using TempDirectory backup = TempDirectory.Create();
+ CreateCompressedPayload(backup.Path, "registry/mouse.reg", "Windows Registry Editor Version 5.00");
+ SnapshotEvent snapshot = NewEvent(SnapshotEventKind.Verified, backup.Path);
+ SnapshotPayloadPreparationService service = new SnapshotPayloadPreparationService();
+
+ SnapshotPayloadPreparation prepared = await service.PrepareAsync(snapshot, CancellationToken.None);
+ string extractedPath = prepared.Path;
+
+ Assert.True(prepared.IsPrepared);
+ Assert.True(File.Exists(Path.Combine(extractedPath, "registry", "mouse.reg")));
+ prepared.Dispose();
+ Assert.False(Directory.Exists(extractedPath));
+}
+
+[Fact]
+public async Task PrepareAsync_FailedSnapshotReturnsDiagnosticWithoutOpeningPayload()
+{
+ SnapshotPayloadPreparation prepared = await new SnapshotPayloadPreparationService().PrepareAsync(
+ NewEvent(SnapshotEventKind.Failed, @"C:\retained-failure"), CancellationToken.None);
+
+ Assert.False(prepared.IsPrepared);
+ Assert.Contains("cannot be selected", prepared.Error, StringComparison.OrdinalIgnoreCase);
+}
+```
+
+- [ ] **Step 2: Run the focused preparation tests and verify they fail**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~SnapshotPayloadPreparationServiceTests`
+
+Expected: FAIL to compile because the preparation DTO and service do not exist.
+
+- [ ] **Step 3: Implement asynchronous preparation without changing Core payload semantics**
+
+Make the result own the Core read scope and expose its path only when preparation succeeds. Do not filter archive entries here: comparison/restore needs the selected snapshot’s complete prepared payload and Core remains the archive validator.
+
+```csharp
+public async Task PrepareAsync(
+ SnapshotEvent snapshot, CancellationToken cancellationToken)
+{
+ if (snapshot == null)
+ throw new ArgumentNullException(nameof(snapshot));
+
+ if (!snapshot.IsRestorable)
+ return new SnapshotPayloadPreparation(snapshot, null,
+ "This backup attempt cannot be selected because it is failed or unreadable.");
+
+ cancellationToken.ThrowIfCancellationRequested();
+ return await Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (!BackupPayload.TryPrepareForRead(snapshot.CanonicalPath, out BackupPayload.ReadScope scope,
+ out string error))
+ {
+ return new SnapshotPayloadPreparation(snapshot, null,
+ "The selected backup payload could not be prepared: " + error);
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ scope.Dispose();
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ return new SnapshotPayloadPreparation(snapshot, scope, null);
+ }, cancellationToken).ConfigureAwait(false);
+}
+```
+
+`SnapshotPayloadPreparation.Dispose()` must be idempotent and call `scope?.Dispose()`. On every failure it holds no scope; its `Error` is the Core error text with context, never an invented “empty snapshot” state.
+
+Pin the DTO invariant in both implementation and tests so a successful loose-folder scope (whose owned extraction path is null) is not misreported as failed:
+
+```csharp
+public string Path => scope?.Path;
+public string Error { get; }
+public bool IsPrepared => Error == null;
+```
+
+- [ ] **Step 4: Run the preparation tests and verify they pass**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~SnapshotPayloadPreparationServiceTests`
+
+Expected: PASS; loose folders keep their original path, compressed folders are removed after disposal, and corrupt/missing payload errors are surfaced verbatim enough to diagnose the actual failure.
+
+- [ ] **Step 5: Commit safe selection preparation**
+
+```bash
+git add src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparation.cs src/WinRestoreKit.Application/Snapshots/SnapshotPayloadPreparationService.cs src/WinRestoreKit.Tests/SnapshotPayloadPreparationServiceTests.cs
+git commit -m "feat: prepare selected snapshot payloads safely"
+```
+
+### Task 4: Project the shared events into Timeline and advanced-history view models
+
+**Files:**
+- Create: `src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventStatus.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/Timeline/TimelineViewModel.cs`
+- Create: `src/WinRestoreKit.Wpf/ViewModels/History/AdvancedHistoryViewModel.cs`
+- Modify: `src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs`
+- Create: `src/WinRestoreKit.Tests/TimelineViewModelTests.cs`
+- Create: `src/WinRestoreKit.Tests/AdvancedHistoryViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/TimelineViewModelTests.cs`
+
+**Interfaces:**
+- Consumes: Task 2 event catalog, Task 3 preparation service, and `ITimelineNavigator` defined in this task.
+- Produces: `SnapshotEventViewModel` as the sole WPF projection of kind, label, icon, diagnostic-only state, and selection eligibility; `TimelineViewModel` exposes the default timeline list and `AdvancedHistoryViewModel` exposes the same source filtered by query. `ITimelineNavigator.OpenCompare` owns a successful preparation; `ShowSnapshotDiagnostic` never receives a preparation.
+
+- [ ] **Step 1: Write failing selection, status, and history-source tests**
+
+Test a constructed catalog/fixture event list, not XAML. Assert one status mapper determines labels for both screens and that rejected statuses never call payload preparation:
+
+```csharp
+[Fact]
+public async Task OpenSelectedAsync_PreparesPartialAndTransfersOwnershipToNavigator()
+{
+ SnapshotEvent partial = NewEvent(SnapshotEventKind.Partial, @"C:\snapshot");
+ RecordingNavigator navigator = new RecordingNavigator();
+ TimelineViewModel viewModel = new TimelineViewModel(
+ new FakeCatalog(partial), new FakePreparationService(partial), navigator);
+
+ await viewModel.RefreshAsync();
+ viewModel.SelectedEvent = Assert.Single(viewModel.Events);
+ await viewModel.OpenSelectedAsync();
+
+ Assert.Same(partial, navigator.Prepared.Snapshot);
+ Assert.Null(navigator.Diagnostic);
+}
+
+[Fact]
+public async Task OpenSelectedAsync_ShowsFailedEvidenceWithoutPreparingPayload()
+{
+ SnapshotEvent failed = NewEvent(SnapshotEventKind.Failed, @"C:\failed", "disk full");
+ FakePreparationService service = new FakePreparationService();
+ RecordingNavigator navigator = new RecordingNavigator();
+ TimelineViewModel viewModel = new TimelineViewModel(new FakeCatalog(failed), service, navigator);
+
+ await viewModel.RefreshAsync();
+ viewModel.SelectedEvent = Assert.Single(viewModel.Events);
+ await viewModel.OpenSelectedAsync();
+
+ Assert.Same(failed, navigator.Diagnostic);
+ Assert.Equal(0, service.Calls);
+}
+
+
+private static SnapshotEvent NewEvent(SnapshotEventKind kind, string path, string reason = null)
+ => new SnapshotEvent(kind, new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Local),
+ Path.GetFileName(path), Path.GetFullPath(path), reason, "TEST-PC", 0, true, null);
+private sealed class FakeCatalog : ISnapshotEventReader
+{
+ private readonly IReadOnlyList events;
+ internal FakeCatalog(params SnapshotEvent[] events) => this.events = events;
+ public IReadOnlyList Read() => events;
+}
+
+private sealed class FakePreparationService : ISnapshotPayloadPreparationService
+{
+ private readonly SnapshotEvent preparedEvent;
+ internal FakePreparationService(SnapshotEvent preparedEvent = null) => this.preparedEvent = preparedEvent;
+ internal int Calls { get; private set; }
+
+ public Task PrepareAsync(SnapshotEvent snapshot, CancellationToken cancellationToken)
+ {
+ Calls++;
+ return Task.FromResult(new SnapshotPayloadPreparation(
+ preparedEvent ?? snapshot, null, preparedEvent == null ? "unexpected preparation" : null));
+ }
+}
+
+private sealed class RecordingNavigator : ITimelineNavigator
+{
+ internal SnapshotPayloadPreparation Prepared { get; private set; }
+ internal SnapshotEvent Diagnostic { get; private set; }
+ public void OpenCompare(SnapshotPayloadPreparation preparation) => Prepared = preparation;
+ public void ShowSnapshotDiagnostic(SnapshotEvent snapshot) => Diagnostic = snapshot;
+}
+```
+
+Add an advanced-history test that filters by display name, machine, canonical path, and kind label while retaining the same `SnapshotEventViewModel.Status` instance/values used by Timeline. The history test must not create a second classifier.
+
+- [ ] **Step 2: Run the focused view-model tests and verify they fail**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~TimelineViewModelTests|FullyQualifiedName~AdvancedHistoryViewModelTests"`
+
+Expected: FAIL to compile because the WPF view-model and navigation types are absent.
+
+- [ ] **Step 3: Implement one presentation projection and explicit selection transfer**
+
+Make `SnapshotEventStatus.For` the only WPF kind-to-UI projection. It supplies words and a Fluent glyph key but does not recalculate whether a snapshot is restorable:
+
+```csharp
+public sealed class SnapshotEventStatus
+{
+ public SnapshotEventStatus(string label, string glyph, bool isDiagnosticOnly)
+ {
+ Label = label;
+ Glyph = glyph;
+ IsDiagnosticOnly = isDiagnosticOnly;
+ }
+
+ public string Label { get; }
+ public string Glyph { get; }
+ public bool IsDiagnosticOnly { get; }
+}
+
+internal static class SnapshotEventStatusProjection
+{
+ internal static SnapshotEventStatus For(SnapshotEventKind kind) => kind switch
+ {
+ SnapshotEventKind.Verified => new("Verified", "CheckmarkCircle", false),
+ SnapshotEventKind.Partial => new("Partial snapshot", "Warning", false),
+ SnapshotEventKind.Failed => new("Backup failed", "ErrorCircle", true),
+ SnapshotEventKind.Unreadable => new("Details unavailable", "ErrorCircle", true),
+ _ => throw new ArgumentOutOfRangeException(nameof(kind))
+ };
+}
+```
+
+`SnapshotEventViewModel` holds the immutable event and this status. `TimelineViewModel.RefreshAsync` replaces its observable collection from one `catalog.Read()` result; its `OpenSelectedAsync` follows this ownership-safe branch:
+
+```csharp
+if (SelectedEvent == null)
+ return;
+
+if (!SelectedEvent.Event.IsRestorable)
+{
+ navigator.ShowSnapshotDiagnostic(SelectedEvent.Event);
+ return;
+}
+
+SnapshotPayloadPreparation prepared = await preparationService
+ .PrepareAsync(SelectedEvent.Event, cancellationToken);
+if (!prepared.IsPrepared)
+{
+ SelectionError = prepared.Error;
+ prepared.Dispose();
+ return;
+}
+
+
+try
+{
+ navigator.OpenCompare(prepared);
+ prepared = null; // navigator now owns it
+}
+finally
+{
+ prepared?.Dispose();
+}
+```
+
+Add ` ` to the WPF project’s existing assembly-friend declarations so test-only `RecordingNavigator` can implement the internal navigation seam. Do not make `ITimelineNavigator` public; make only types/properties bound from XAML public, as specified in **Produced Interfaces**.
+
+`AdvancedHistoryViewModel` receives the same catalog instance and uses `ICollectionView` filtering over `SnapshotEventViewModel`; it exposes exact timestamp, machine, canonical path, manifest state label, byte count/unknown size, and diagnostic reason. It never reads files or parses manifest/log/payload content.
+
+- [ ] **Step 4: Run the view-model tests and verify they pass**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~TimelineViewModelTests|FullyQualifiedName~AdvancedHistoryViewModelTests"`
+
+Expected: PASS; verified/partial transfer a prepared scope, failed/unreadable are diagnostic-only, failed preparation is inline evidence, and both screens use the one status projection.
+
+- [ ] **Step 5: Commit the WPF event projection**
+
+```bash
+git add src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs src/WinRestoreKit.Wpf/Navigation/ITimelineNavigator.cs src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventStatus.cs src/WinRestoreKit.Wpf/ViewModels/Snapshots/SnapshotEventViewModel.cs src/WinRestoreKit.Wpf/ViewModels/Timeline/TimelineViewModel.cs src/WinRestoreKit.Wpf/ViewModels/History/AdvancedHistoryViewModel.cs src/WinRestoreKit.Tests/TimelineViewModelTests.cs src/WinRestoreKit.Tests/AdvancedHistoryViewModelTests.cs
+git commit -m "feat: add timeline and advanced history view models"
+```
+
+### Task 5: Render accessible Timeline and advanced History from the shared projection
+
+**Files:**
+- Create: `src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/TimelineView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/TimelineView.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml`
+- Create: `src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Resources/SnapshotEventTemplates.xaml`
+- Create: `src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs`
+- Test: `src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs`
+
+**Interfaces:**
+- Consumes: Task 4’s `TimelineViewModel`, `AdvancedHistoryViewModel`, `SnapshotEventViewModel`, and one status projection.
+- Produces: keyboard-operable views that bind only to view-model properties. `SnapshotEventList` is the common visual-timeline/list-fallback control used by both screens; it has a standard `ListBox` UIA surface and never contains filesystem or status-classification code.
+
+- [ ] **Step 1: Write failing STA accessibility and construction tests**
+
+Use Foundation’s sole `src/WinRestoreKit.Tests/WpfTestHost.cs` helper, `WpfTestHost.Run(Action)`, to construct the actual view:
+
+```csharp
+[Fact]
+public void TimelineView_ExposesEquivalentNamedListAndKeyboardSelection()
+{
+ WpfTestHost.Run(() =>
+ {
+ TimelineView view = new TimelineView { DataContext = NewTimelineViewModel() };
+ view.ApplyTemplate();
+
+ ListBox list = Assert.IsType(view.FindName("TimelineEventList"));
+ Assert.Equal("Snapshots", AutomationProperties.GetName(list));
+ Assert.Equal(SelectionMode.Single, list.SelectionMode);
+ Assert.True(KeyboardNavigation.GetDirectionalNavigation(list) == KeyboardNavigationMode.Continue);
+ Assert.Contains("Enter", AutomationProperties.GetHelpText(list));
+ list.SelectedIndex = 0;
+ RaiseKey(list, Key.Right);
+ Assert.Equal(1, list.SelectedIndex);
+ RaiseKey(list, Key.Left);
+ Assert.Equal(0, list.SelectedIndex);
+ });
+}
+```
+
+Add this test-local key helper; `NewTimelineViewModel()` creates two events through the `ISnapshotEventReader`/preparation-service test doubles defined in Task 4:
+
+```csharp
+private static void RaiseKey(UIElement target, Key key)
+{
+ target.RaiseEvent(new KeyEventArgs(Keyboard.PrimaryDevice, null, 0, key)
+ {
+ RoutedEvent = Keyboard.PreviewKeyDownEvent
+ });
+}
+```
+
+Also assert each row’s `AutomationProperties.Name` includes its title, formatted timestamp, text status, and `DiagnosticReason` when diagnostic-only; assert the inline selection-error `TextBlock` has `AutomationProperties.LiveSetting="Polite"`.
+
+- [ ] **Step 2: Run the focused WPF tests and verify they fail**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~TimelineAccessibilityTests`
+
+Expected: FAIL to compile because the Timeline WPF views do not exist.
+
+- [ ] **Step 3: Implement reusable list/list-fallback XAML and input semantics**
+
+Use an ordinary `ListBox` for the UIA-equivalent narrow representation; it must remain the accessible control even when the wide visual timeline applies an item template with a restrained connecting line. Bind the common collection and avoid duplicate row templates.
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+In `OnPreviewKeyDown`, change `SelectedIndex` by `-1` for `Key.Left` and `+1` for `Key.Right`, constrain it to `[0, Items.Count - 1]`, set `e.Handled = true`, and call `ScrollIntoView`; let Up/Down, Tab, Shift+Tab, and standard `ListBox` selection behavior remain native. On Enter execute the view model’s async open command. In a width trigger at 1024px, change only layout and connector visibility; retain the same `ListBox`, bindings, item text, commands, names, and focus behavior rather than maintaining a second data source.
+
+Use `SnapshotEventTemplates.xaml` from both Timeline and History to bind `Status.Label`, not local `DataTrigger` status text. `TimelineView` contains the error text shown after failed payload preparation:
+
+```xml
+
+```
+
+`AdvancedHistoryView` binds the same event rows in a searchable `ListView`/`GridView` (timestamp, machine, path, manifest status, size, diagnostics), preserving the shared status template and no Restore button.
+
+Use Foundation’s existing `WpfTestHost.Run(Action)`; do not create another STA helper.
+
+- [ ] **Step 4: Run the accessibility tests and verify they pass**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~TimelineAccessibilityTests`
+
+Expected: PASS; WPF views construct on an STA thread, the list exposes a stable UIA name/help text, all state is textually distinguishable, and narrow/wide layouts share the same accessible item collection.
+
+- [ ] **Step 5: Commit the WPF Timeline and advanced History views**
+
+```bash
+git add src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml src/WinRestoreKit.Wpf/Views/Controls/SnapshotEventList.xaml.cs src/WinRestoreKit.Wpf/Views/TimelineView.xaml src/WinRestoreKit.Wpf/Views/TimelineView.xaml.cs src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml src/WinRestoreKit.Wpf/Views/AdvancedHistoryView.xaml.cs src/WinRestoreKit.Wpf/Resources/SnapshotEventTemplates.xaml src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs
+git commit -m "feat: render accessible snapshot timeline"
+```
+
+### Task 6: Verify the Timeline WPF runtime path without comparison scope
+
+**Files:**
+- Create: `src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs`
+- Test: `src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs`
+
+**Interfaces:**
+- Consumes: Tasks 2–5. The test host supplies a real `TimelineViewModel`, catalog, preparation service, and recording `ITimelineNavigator`.
+- Produces: runtime evidence that the real Timeline WPF control can load event data, render an accessible list, and transfer a verified/partial preparation to its navigation boundary. Task 5 supplies the separate explicit keyboard-movement test. The Compare/Confirm plan owns production shell-stage composition and the long-lived preparation after this boundary.
+
+- [ ] **Step 1: Write the failing WPF runtime smoke test**
+
+```csharp
+[Fact]
+public void TimelineView_LoadsSelectionAndTransfersPreparedSnapshot()
+{
+ WpfTestHost.Run(() =>
+ {
+ RecordingNavigator navigator = new RecordingNavigator();
+ SnapshotEvent snapshot = NewEvent(SnapshotEventKind.Verified, @"C:\timeline-smoke");
+ TimelineViewModel viewModel = new TimelineViewModel(
+ new FakeCatalog(snapshot), new FakePreparationService(snapshot), navigator);
+ TimelineView view = new TimelineView { DataContext = viewModel };
+ Window host = new Window { Content = view, Width = 1024, Height = 720 };
+
+ host.Show();
+ try
+ {
+ viewModel.RefreshAsync().GetAwaiter().GetResult();
+ ListBox list = FindDescendant(view);
+ list.SelectedIndex = 0;
+ viewModel.OpenSelectedAsync().GetAwaiter().GetResult();
+
+ Assert.NotNull(navigator.Prepared);
+ Assert.Equal(SnapshotEventKind.Verified, navigator.Prepared.Snapshot.Kind);
+ }
+ finally
+ {
+ navigator.Prepared?.Dispose();
+ host.Close();
+ }
+ });
+}
+```
+
+Add this test-local visual-tree helper in `TimelineWpfSmokeTests.cs`:
+
+```csharp
+private static T FindDescendant(DependencyObject root) where T : DependencyObject
+{
+ if (root is T matched)
+ return matched;
+
+ for (int index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++)
+ {
+ T found = FindDescendant(VisualTreeHelper.GetChild(root, index));
+ if (found != null)
+ return found;
+ }
+
+ return null;
+}
+```
+
+- [ ] **Step 2: Run the runtime smoke test and verify it fails**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter FullyQualifiedName~TimelineWpfSmokeTests`
+
+Expected: FAIL because the Timeline control cannot yet be hosted with a populated view model and transfer a selected preparation.
+
+- [ ] **Step 3: Correct real-control binding, layout, and command wiring**
+
+Make only the Task 4/Task 5 controls work under a shown WPF `Window`: use `Loaded`/dispatcher-safe collection refresh, ensure the `ListBox` receives keyboard focus after the test selects it, and bind the Enter handler to `OpenSelectedAsync`. The recording navigator must be a test double only; production ownership and navigation are deliberately implemented by the next Compare/Confirm plan. Do not add a comparison surface, a fake “coming soon” state, a restore-set collection, or a confirmation dialog.
+
+- [ ] **Step 4: Run all focused Timeline tests and the WPF runtime smoke test**
+
+Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~SnapshotEventCatalogTests|FullyQualifiedName~SnapshotPayloadPreparationServiceTests|FullyQualifiedName~TimelineViewModelTests|FullyQualifiedName~AdvancedHistoryViewModelTests|FullyQualifiedName~TimelineAccessibilityTests|FullyQualifiedName~TimelineWpfSmokeTests"`
+
+Expected: PASS; event classification, selection cleanup, shared status projection, keyboard/UIA construction, and the shown-window Timeline runtime path all pass.
+
+- [ ] **Step 5: Commit Timeline runtime verification coverage**
+
+```bash
+git add src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs
+git commit -m "test: smoke timeline WPF runtime path"
+```
+## Final Verification
+
+- [ ] Run: `dotnet test src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~BackupFoldersReadTests|FullyQualifiedName~SnapshotEventCatalogTests|FullyQualifiedName~SnapshotPayloadPreparationServiceTests|FullyQualifiedName~TimelineViewModelTests|FullyQualifiedName~AdvancedHistoryViewModelTests|FullyQualifiedName~TimelineAccessibilityTests|FullyQualifiedName~TimelineWpfSmokeTests"`
+
+Expected: PASS with no failing selected Timeline/event tests.
+
+- [ ] Run: `dotnet build src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj`
+
+Expected: Build succeeds; `WinRestoreKit.Application` compiles without `UseWindowsForms` or `UseWPF`, and WPF references the event DTO rather than a `Views` discovery type.
+
+- [ ] Run: `dotnet build src/WinRestoreKit.sln`
+
+Expected: Build succeeds with both the legacy WinForms shell and side-by-side WPF project resolving one Application-owned backup-folder discovery implementation.
+
+- [ ] On a real Windows desktop, perform Task 5’s keyboard/UIA smoke and Task 6’s shown-window preparation smoke with a compressed snapshot, a failed retained folder, a malformed manifest, and a session-only folder-creation failure. Expected: only verified/partial events can start payload preparation; Left/Right changes the selected accessible list item; all diagnostic text reflects actual evidence; closing/canceling a successful compressed selection removes its private extraction directory; restarting the app removes only the session-only failure from Timeline and does not alter any backup retention state.
diff --git a/docs/superpowers/plans/2026-08-09-wpf-cutover-release-verification.md b/docs/superpowers/plans/2026-08-09-wpf-cutover-release-verification.md
new file mode 100644
index 0000000..011d4ab
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-wpf-cutover-release-verification.md
@@ -0,0 +1,1407 @@
+# WPF Cutover, Release, and Verification Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Verify the completed WPF workflows on a real Windows desktop, make WPF the sole shipping `WinRestoreKit` application without compatibility shims, and prove that the released artifact is one self-contained, version-coherent `WinRestoreKit.exe`.
+
+**Architecture:** This is the fifth and final migration stage. It consumes the framework-neutral Application layer and the WPF Timeline, Compare/Confirm, and Backup/Progress outputs; it does not redesign their behavior. First make the existing WPF shell observable and testable, then complete the real-desktop parity gate, switch the sole shipping identity and test references to WPF, remove every obsolete WinForms artifact, and finally run the release procedure against the published executable.
+
+**Tech Stack:** .NET 8, C#, WPF/MVVM, xUnit 2.9.3, WPF Dispatcher and UI Automation peers, Windows accessibility tooling, PowerShell, Windows SDK `mt.exe`, GitHub Releases, self-contained `win-x64` single-file publishing.
+
+## Global Constraints
+
+- Preserve `WinRestoreKit.Core` backup/restore semantics, the on-disk snapshot and manifest format, `RestorePlan`, `SnapshotGate`, `RestoreScope`, `RestoreDispatch`, `ExplorerRestartPrompt`, backup-root behavior, and all existing outcome meanings.
+- Execute this plan only after the Foundation, Timeline, Compare/Confirm, and Backup/Progress plans below have completed their focused test cycles and their WPF equivalents are runnable alongside WinForms.
+- `WinRestoreKit.Application` remains framework-neutral: it references neither WinForms nor WPF, keeps namespace `WinRestoreKit`, and retains its own `InternalsVisibleTo("WinRestoreKit.Tests")` access boundary.
+- `RunSummary` uses `RunSeverity { Information, Warning, Error }`. `IRunUi` has no `MessageBoxIcon`, `IWin32Window`, or typed `Owner`; it exposes only `object DialogOwner { get; }` so the existing Core app-restore seam stays framework-neutral and opaque.
+- Timeline events retain `SnapshotEventKind { Verified, Partial, Failed, Unreadable }`; sort deterministically by descending timestamp and then ordinal canonical path; failed and unreadable events remain non-restorable; session-only failures never change cleanup retention.
+- Compare continues to be manifest-first with the exact `RestoreContents` artifact-precedence rules, maps `HasDriftedFrom` `true`/`false`/`null` to `Changed`/`Same`/`Unavailable`, marks only the throwing module unavailable, and always disposes its `BackupPayload.ReadScope`.
+- WPF never parses registry exports or payload text and never invents semantic comparison values. Restore remains whole-module and its confirmation reports only process closures, Explorer restart, and sign-out impacts that existing contracts declare; do not introduce reboot metadata.
+- Do not delete WinForms until the real Windows desktop smoke matrix in Task 2 has passed and its evidence is committed. A build, unit test, or screenshot alone is not a substitute for this gate.
+- The final WPF app has assembly and executable identity `WinRestoreKit`, preserves `highestAvailable`, `longPathAware`, `WinRestoreKit.ico`, and the physical raw fallback source `src/WinRestoreKit/Properties/AssemblyInfo.cs`.
+- The raw fallback source retains the exact three-part `[assembly: AssemblyFileVersion("x.y.z")]` line. `GenerateAssemblyInfo=false` appears only on the final shipping WPF project; `Core` and `Application` keep SDK-generated assembly metadata.
+- The only shipping release is self-contained `win-x64`, single-file, native-self-extracting, compressed, and untrimmed. The publish directory contains exactly one `WinRestoreKit.exe`; do not ship a framework-dependent `bin\Release` executable.
+- Preserve relevant Core and Application tests. Replace tests that only construct or inspect WinForms controls with observable WPF contract tests; do not mechanically port control-tree assertions.
+- Do not commit `bin/`, `obj/`, `publish/`, temporary snapshot roots, extracted manifests, screenshots outside the committed baseline directory, or release-verification scratch output.
+
+---
+
+## Upstream Interfaces and Required Completed Outputs
+
+This plan may consume the preceding plans only through this block. Do not reimplement these services in Stage 5.
+
+| Upstream plan | Consumed files and interfaces | Cutover responsibility |
+| --- | --- | --- |
+| `docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md` | `src/WinRestoreKit.Application/`; `RunSeverity`; neutral `RunSummary`; neutral `IRunUi`; moved `RunCoordinator`, `RunControl`, `BackupRestoreOrchestrator`, `ProgressMetrics`; `Application/Updates/VersionInfo.cs`, `UpdateVerdict.cs`, `UpdateCheckService.cs`; `Application/Settings/BackupRootRegistry.cs`; `WinRestoreKit.Wpf/App.xaml`, `App.xaml.cs`, `MainWindow.xaml`, `ViewModels/ShellViewModel.cs`, WPF dispatcher/dialog/log/theme/update adapters | Keep Application framework-neutral; retain WPF MVVM composition and update/version behavior while changing WPF from its temporary identity to the shipping identity. |
+| `docs/superpowers/plans/2026-08-09-timeline-event-model.md` | `SnapshotEventKind`; immutable `SnapshotEvent`; `SnapshotEventCatalog.Read()`; `SnapshotEventCatalog.RecordSessionFailure(DateTime created, string displayName, string diagnosticReason)`; `SnapshotPayloadPreparationService.PrepareAsync(SnapshotEvent snapshot, CancellationToken cancellationToken)`; `TimelineViewModel`; `AdvancedHistoryViewModel`; `TimelineView.xaml`; `AdvancedHistoryView.xaml`; shared `WpfTestHost.cs`; Timeline accessibility and smoke tests | Reuse the same event catalog for Timeline and Advanced History; retain the session-failure rule and the Timeline `ListBox` automation name `Snapshots`; extend the sole STA helper and runtime coverage rather than introducing another one. |
+| `docs/superpowers/plans/2026-08-09-compare-confirm-restore.md` | `ComparisonState { Changed, Same, Unavailable, NotCaptured }`; immutable `ModuleComparison`; `Task> SnapshotComparisonService.CompareAsync(SnapshotEvent snapshot, IReadOnlyList modules, CancellationToken cancellationToken)`; `ComparisonWorkspaceViewModel`; `ModuleComparisonRowViewModel`; `RestoreSetViewModel`; `ConfirmViewModel`; `ComparisonWorkspaceView`; `ConfirmView`; `RestoreConsentDialog`; `WpfRunUi` dialog wiring | Verify all comparison evidence states and cancellation at runtime, preserve whole-module restore selection, and verify WPF-owned modal ownership without reintroducing a typed WinForms owner seam. |
+| `docs/superpowers/plans/2026-08-09-backup-progress-results.md` | `BackupWorkspaceViewModel`; `ProgressWorkspaceViewModel`; `ResultWorkspaceViewModel`; `BackupWorkspaceView`; `ProgressWorkspaceView`; `ResultWorkspaceView`; `WpfRunUi`; `WpfLogSink`; `WpfAppRestoreDialog`; Application-side `ScopeGroups`, `BackupPresets`, `BackupFolders` | Verify Create snapshot → Progress → Results → Timeline end to end, preserve app-restore behavior, and remove the corresponding WinForms implementations only after their WPF contracts pass. |
+
+### Stage-5 invariants to pin before removal
+
+- `SnapshotEventCatalog.Read()` is the sole persisted Timeline/Advanced History source. A failed session event comes only from `RecordSessionFailure`, is diagnostic-only, and does not create or preserve a cleanup-retained folder.
+- `SnapshotComparisonService.CompareAsync(SnapshotEvent snapshot, IReadOnlyList modules, CancellationToken cancellationToken)` remains read-only. Every temporary payload from `SnapshotPayloadPreparationService.PrepareAsync(SnapshotEvent snapshot, CancellationToken cancellationToken)` is released when the Compare workspace cancels, changes snapshot, or closes.
+- `IRunUi` carries progress, summary, consent, snapshot-override, plan-error, Explorer-restart, and opaque `object DialogOwner { get; }` without a WinForms type. The Application orchestrator preserves the Core call `await appStoreApps.RestoreAsync(currentRestorePath, ui.DialogOwner)`; WPF supplies its shell `Window` as the opaque owner and registers `WpfAppRestoreDialog` through Core's existing `RestoreDialog` delegate.
+- `src/WinRestoreKit.Tests/WpfTestHost.cs` is the sole STA smoke helper. It constructs a named STA thread, exposes `Run(Action)` and `Run(Func)`, captures and rethrows failures, shuts down the thread's `Dispatcher`, and joins the thread. Tests construct `ShellViewModel` with Foundation test fakes and pass it directly to `new MainWindow(shell)`; they do not use reflection or add a second application startup path.
+- WPF automation identifiers are stable contracts, not layout details: `TimelineEventList`, `ComparisonWorkspace`, `CompareModuleList`, `CompareFilterAll`, `CompareFilterChanged`, `RestoreSetList`, `CompareContinueToConfirmButton`, `ConfirmRestoreButton`, `CreateSnapshotButton`, `SettingsThemeFollowSystem`, `SettingsThemeLight`, and `SettingsThemeDark`.
+
+---
+
+### Task 1: Make the completed WPF shell testable on one STA host and pin its observable runtime contract
+
+**Files:**
+- Create: `src/WinRestoreKit.Tests/WpfCutoverRuntimeTests.cs`
+- Modify: `src/WinRestoreKit.Tests/WpfTestHost.cs`
+- Modify: `src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs`
+- Modify: `src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj`
+- Modify: `src/WinRestoreKit.Wpf/MainWindow.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/TimelineView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/ConfirmView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/SettingsView.xaml`
+- Test: `src/WinRestoreKit.Tests/TimelineWpfSmokeTests.cs`
+- Test: `src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs`
+- Test: `src/WinRestoreKit.Tests/WpfCutoverRuntimeTests.cs`
+
+**Interfaces:**
+- Consumes: Foundation `internal static WpfTestHost.Run(Action action)` and `WpfTestHost.Run(Func action)`; `ShellViewModel(IThemeService themes, WpfUpdatePresenter updates, string currentVersion)`, internal `NavigateTo(object workspace, string workflowLabel)`, `ShowTimeline()`, `ShowSettingsCommand`, and `CreateSnapshotCommand`; `MainWindow(ShellViewModel shell)`; WPF test friendship; direct `TimelineView`, `ComparisonWorkspaceView`, and `ConfirmView` hosts; and the Foundation `IThemeService`, `IUpdateCheckService`, `IWpfDialogService`, and `IExternalLinkService` contracts.
+- Produces: exactly one reusable STA helper, stable UI Automation identifiers and names, and runtime tests that construct the real WPF shell, navigate actual WPF workspace controls through it, assert focusability and automation roles, and never touch a registry, snapshot, network endpoint, backup run, or dialog.
+
+- [ ] **Step 1: Write the failing WPF shell automation-contract test**
+
+ Add `WpfCutoverRuntimeTests.cs`. Construct `ShellViewModel` through the Foundation test fakes—not a reflection factory and not `App.OnStartup`—then host the real `MainWindow` in `WpfTestHost`. The initial test must fail until Compare, Confirm, and Settings expose the required automation identifiers.
+
+ ```csharp
+ using System;
+ using System.Threading;
+ using System.Runtime.ExceptionServices;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Automation;
+ using System.Windows.Threading;
+ using System.Windows.Automation.Peers;
+ using System.Windows.Controls;
+ using System.Windows.Media;
+ using WinRestoreKit;
+ using WinRestoreKit.Wpf;
+ using WinRestoreKit.Wpf.Services;
+ using WinRestoreKit.Wpf.ViewModels;
+ using WinRestoreKit.Wpf.Views;
+ using Xunit;
+
+ namespace WinRestoreKit.Tests
+ {
+ public sealed class WpfCutoverRuntimeTests
+ {
+ [Fact]
+ public void MainWindow_ConstructsOnStaAndExposesThePrimaryAutomationSurface()
+ {
+ WpfTestHost.Run(() =>
+ {
+ ShellViewModel shell = CreateShell();
+ MainWindow window = new MainWindow(shell);
+ window.Show();
+
+ try
+ {
+ Navigate(shell, window, new TimelineView(), "Timeline");
+ ListBox timeline = Require(window, "TimelineEventList");
+ Assert.Equal("Snapshots", AutomationProperties.GetName(timeline));
+ Assert.True(timeline.Focusable);
+ AssertControlType(timeline, AutomationControlType.List);
+
+ Navigate(shell, window, new ComparisonWorkspaceView(), "Compare");
+ ComparisonWorkspaceView comparison = Require(
+ window, "ComparisonWorkspace");
+ Assert.Equal("Snapshot comparison workspace",
+ AutomationProperties.GetName(comparison));
+ ListBox modules = Require(window, "CompareModuleList");
+ Assert.Equal("Modules in the selected snapshot",
+ AutomationProperties.GetName(modules));
+ AssertControlType(modules, AutomationControlType.List);
+ RadioButton all = Require(window, "CompareFilterAll");
+ Assert.Equal("Show all compared modules", AutomationProperties.GetName(all));
+ Assert.True(all.Focusable);
+ RadioButton changed = Require(window, "CompareFilterChanged");
+ Assert.Equal("Show only changed modules",
+ AutomationProperties.GetName(changed));
+ Assert.True(changed.Focusable);
+ Assert.True(Require(window, "RestoreSetList").Focusable);
+ Assert.True(Require(window,
+ "CompareContinueToConfirmButton").Focusable);
+
+ Navigate(shell, window, new ConfirmView(), "Confirm");
+ Button confirm = Require(window, "ConfirmRestoreButton");
+ Assert.Equal("Continue to final restore consent",
+ AutomationProperties.GetName(confirm));
+ Assert.True(confirm.Focusable);
+ AssertControlType(confirm, AutomationControlType.Button);
+
+ shell.ShowSettingsCommand.Execute(null);
+ window.UpdateLayout();
+ Assert.True(Require(window,
+ "SettingsThemeFollowSystem").Focusable);
+ Assert.True(Require(window, "SettingsThemeLight").Focusable);
+ Assert.True(Require(window, "SettingsThemeDark").Focusable);
+
+ shell.ShowTimeline();
+ shell.CreateSnapshotCommand.Execute(null);
+ window.UpdateLayout();
+ Button createSnapshot = Require(window, "CreateSnapshotButton");
+ Assert.Equal("Create snapshot",
+ AutomationProperties.GetName(createSnapshot));
+ Assert.True(createSnapshot.Focusable);
+ AssertControlType(createSnapshot, AutomationControlType.Button);
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
+ private static ShellViewModel CreateShell()
+ {
+ WpfUpdatePresenter updates = new WpfUpdatePresenter(
+ new FakeUpdates(),
+ new FakeDialogs(),
+ new FakeLinks());
+ return new ShellViewModel(new FakeThemeService(), updates, "0.0.1");
+ }
+
+ private static void Navigate(ShellViewModel shell, MainWindow window,
+ FrameworkElement workspace, string workflowLabel)
+ {
+ shell.NavigateTo(workspace, workflowLabel);
+ window.UpdateLayout();
+ }
+
+ private static T Require(DependencyObject root, string automationId)
+ where T : FrameworkElement
+ {
+ T found = Find(root, automationId);
+ Assert.NotNull(found);
+ return found;
+ }
+
+ private static void AssertControlType(FrameworkElement element,
+ AutomationControlType expected)
+ {
+ AutomationPeer peer = UIElementAutomationPeer.CreatePeerForElement(
+ (UIElement)element);
+ Assert.NotNull(peer);
+ Assert.Equal(expected, peer.GetAutomationControlType());
+ }
+
+ private static T Find(DependencyObject root, string automationId)
+ where T : FrameworkElement
+ {
+ if (root is T current
+ && AutomationProperties.GetAutomationId(current) == automationId)
+ return current;
+
+ for (int index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++)
+ {
+ T found = Find(VisualTreeHelper.GetChild(root, index), automationId);
+ if (found != null)
+ return found;
+ }
+
+ return null;
+ }
+
+ private sealed class FakeThemeService : IThemeService
+ {
+ public ThemeMode Mode { get; private set; } = ThemeMode.FollowSystem;
+ public ThemeMode EffectiveMode { get; private set; } = ThemeMode.Light;
+ public event EventHandler ThemeChanged;
+
+ public void SetMode(ThemeMode mode)
+ {
+ Mode = mode;
+ EffectiveMode = mode == ThemeMode.FollowSystem ? ThemeMode.Light : mode;
+ ThemeChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Dispose() { }
+ }
+
+ private sealed class FakeUpdates : IUpdateCheckService
+ {
+ public Task CheckAsync(string currentVersion,
+ CancellationToken cancellationToken)
+ => Task.FromResult(new UpdateCheckResult(
+ UpdateVerdict.UpToDate, currentVersion, currentVersion));
+ }
+
+ private sealed class FakeDialogs : IWpfDialogService
+ {
+ public void ShowInformation(string text, string caption) { }
+ public void ShowWarning(string text, string caption) { }
+ public void ShowError(string text, string caption) { }
+ public bool Confirm(string text, string caption) => false;
+ }
+
+ private sealed class FakeLinks : IExternalLinkService
+ {
+ public void Open(string url) { }
+ }
+ }
+ }
+ ```
+ The test uses the internal navigation seam only to host each concrete, production `UserControl` in the real `MainWindow`; it does not manufacture a replacement window or execute a workflow. Timeline, Compare, Confirm, backup, and dialog behavior remains covered by their upstream view-model tests and the Task 2 desktop session.
+
+
+- [ ] **Step 2: Run the new test to verify it fails before all WPF automation surfaces exist**
+
+
+ Run:
+
+ ```powershell
+ dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj -c Debug --filter FullyQualifiedName~WpfCutoverRuntimeTests
+ ```
+
+ Expected: the test fails because one or more required Compare, Confirm, or Settings automation identifiers are absent. Do not accept a hand-built fake window as a substitute for the real `ShellViewModel` and `MainWindow`.
+
+- [ ] **Step 3: Give the shared test assembly temporary dual-framework support while WinForms remains runnable**
+
+ During Tasks 1–2, WinForms tests and WPF tests must both compile. Keep the existing WinForms reference and add WPF/Application support; do not remove the WinForms property or project reference until Task 3 deletes every test that depends on it.
+
+ ```xml
+
+ net8.0-windows
+ true
+ true
+ false
+ true
+ disable
+ disable
+ AnyCPU
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ ```
+
+ This dual-reference state is migration-only. It keeps the tree green while real-desktop parity is being proven; it is removed atomically with the obsolete tests and project in Task 3.
+
+- [ ] **Step 4: Keep `WpfTestHost` as the sole STA helper**
+
+ `WpfTestHost` is the sole STA helper from Foundation through cutover. Update Timeline WPF smoke tests to call `WpfTestHost.Run(Action)`; do not create, delete, or reference a second STA helper.
+
+ ```csharp
+ using System;
+ using System.Runtime.ExceptionServices;
+ using System.Threading;
+ using System.Windows.Threading;
+
+ internal static class WpfTestHost
+ {
+ internal static T Run(Func action)
+ {
+ T result = default;
+ Exception failure = null;
+ Thread thread = new Thread(() =>
+ {
+ try
+ {
+ result = action();
+ }
+ catch (Exception ex)
+ {
+ failure = ex;
+ }
+ finally
+ {
+ Dispatcher.CurrentDispatcher.InvokeShutdown();
+ }
+ });
+
+ thread.Name = "WinRestoreKit WPF test STA";
+ thread.IsBackground = true;
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+
+ if (failure != null)
+ ExceptionDispatchInfo.Capture(failure).Throw();
+ return result;
+ }
+ }
+ ```
+
+ Retain the existing `Run(Action action)` overload as a thin wrapper around `Run(() => { action(); return null; })`. Do not use `Application.Run`, create a second `Application`, or leave a Dispatcher alive after a test.
+
+- [ ] **Step 5: Pin automation names, roles, focusability, and live status in the actual WPF views**
+
+ Add the stable UIA surface to the existing controls; do not create hidden duplicate controls merely to satisfy a test. The values below are intentionally user-facing and testable.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ For each event row, compose its automation name from title, timestamp, event state, and diagnostic reason when one exists. Mark the real inline error/status element with `AutomationProperties.LiveSetting="Polite"`; failed and unreadable entries remain disabled for restoration rather than disappearing.
+
+- [ ] **Step 6: Run the focused STA, Timeline, and accessibility tests to verify they pass**
+
+ Run:
+
+ ```powershell
+ dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj -c Debug --filter "FullyQualifiedName~WpfCutoverRuntimeTests|FullyQualifiedName~TimelineWpfSmokeTests|FullyQualifiedName~TimelineAccessibilityTests"
+ ```
+
+ Expected: all selected tests pass, no test opens a visible window indefinitely, and output reports `Failed: 0`. At this point the WinForms tests remain present and runnable under the temporary dual-reference project.
+
+- [ ] **Step 7: Commit the reusable WPF runtime-test surface**
+
+ ```powershell
+ git add src\WinRestoreKit.Tests\WpfTestHost.cs src\WinRestoreKit.Tests\WpfCutoverRuntimeTests.cs src\WinRestoreKit.Tests\TimelineWpfSmokeTests.cs src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj src\WinRestoreKit.Wpf\MainWindow.xaml src\WinRestoreKit.Wpf\Views\TimelineView.xaml src\WinRestoreKit.Wpf\Views\ComparisonWorkspaceView.xaml src\WinRestoreKit.Wpf\Views\ConfirmView.xaml src\WinRestoreKit.Wpf\Views\SettingsView.xaml
+ git commit -m "test: add WPF cutover runtime coverage"
+ ```
+### Task 2: Execute and record the real Windows parity, accessibility, responsive, theme, and visual-baseline gate
+
+**Files:**
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover.md`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-normal-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-normal-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-partial-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-partial-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-failed-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-failed-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-unreadable-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-unreadable-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/compare-all-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/compare-all-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/confirm-light-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/confirm-dark-100.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-normal-light-150.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/compare-all-light-150.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/confirm-light-150.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/timeline-normal-light-200.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/compare-all-light-200.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/confirm-light-200.png`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-cutover/screenshots/compare-all-light-1024w.png`
+- Modify: `src/WinRestoreKit.Wpf/Views/TimelineView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/ComparisonWorkspaceView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/ConfirmView.xaml`
+- Modify: `src/WinRestoreKit.Wpf/Views/SettingsView.xaml`
+- Test: `src/WinRestoreKit.Tests/WpfCutoverRuntimeTests.cs`
+- Test: `src/WinRestoreKit.Tests/TimelineAccessibilityTests.cs`
+- Test: `src/WinRestoreKit.Tests/ComparisonWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ConfirmViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs`
+
+**Interfaces:**
+- Consumes: one application-lifetime `SnapshotEventCatalog`, `TimelineViewModel.RefreshAsync()`, `SnapshotPayloadPreparationService.PrepareAsync(SnapshotEvent snapshot, CancellationToken cancellationToken)`, `SnapshotComparisonService.CompareAsync(SnapshotEvent snapshot, IReadOnlyList modules, CancellationToken cancellationToken)`, comparison and restore-set view models, `WpfRunUi`, `WpfDialogService`, `WpfAppRestoreDialog`, `IThemeService`, and the Task 1 automation identifiers.
+- Produces: a committed real-desktop verification record and deterministic screenshot baselines proving the WPF shell is usable before destructive source removal. It produces no new product workflow, snapshot format, comparison evidence, restore rule, or compatibility shell.
+
+- [ ] **Step 1: Write the verification record before running the desktop session**
+
+ Create `docs/superpowers/verification/2026-08-09-wpf-cutover.md` with these immutable headings and a results table under each heading: `Environment`, `Build under test`, `Snapshot fixtures`, `Workflow parity`, `Keyboard and UI Automation`, `Themes and reduced motion`, `DPI and responsive layout`, `Screenshot baselines`, `Publish smoke prerequisites`, and `Result`.
+
+ Record all of the following before taking screenshots:
+
+ | Legacy workflow that must be absent after cutover | WPF workflow that must be observed now | Required result |
+ | --- | --- | --- |
+ | `MainForm` rail and `HomePageView` dashboard | `MainWindow` top bar and default Timeline workspace | No persistent sidebar or dashboard metric-strip home is present. |
+ | `RestoreWizardStep1View` | Timeline event selection and narrow list fallback | Verified and Partial entries enter Compare; Failed and Unreadable entries expose diagnostics only. |
+ | `RestoreWizardStep2View` | Comparison workspace and whole-module restore set | All/Changed-only filtering retains Unavailable and Not captured evidence under All; selection never descends below a module. |
+ | `RestoreConfirmForm` | Confirm workspace plus `RestoreConsentDialog` | Existing plan, snapshot gate, process/Explorer/sign-out impacts, and incomplete-snapshot consent are presented before a write. |
+ | `BackupPageView` and `ScopeGroups` | Create snapshot and WPF backup selection | Existing preset, scope, containment, naming, and compression choices reach the unchanged Application orchestration. |
+ | `ProgressPageView`, `ProgressLogSink`, `RichTextBoxLogSink` | WPF progress/result views and WPF dispatcher/log adapters | Progress, pause/cancel, logs, late-cancel wording, and final summary remain observable. |
+ | `HistoryPageView` | Timeline plus Advanced History | Both projections show the same events and source paths. |
+ | `AboutPageView`, `Theme`, `UpdateCheck` | WPF About, settings, theme and update adapters | Light, Dark, Follow system, version display, update status, and safe external link behavior are present. |
+ | `RestAppsForm` | `WpfAppRestoreDialog` | Existing app-export source and outcome behavior is reachable from a WPF-owned dialog. |
+
+- [ ] **Step 2: Build the side-by-side WPF application and run its focused deterministic tests**
+
+ Run:
+
+ ```powershell
+ dotnet build src\WinRestoreKit.sln -c Debug
+ dotnet test src\WinRestoreKit.sln -c Debug --no-build --filter "FullyQualifiedName~Timeline|FullyQualifiedName~SnapshotComparison|FullyQualifiedName~ComparisonWorkspace|FullyQualifiedName~RestoreSet|FullyQualifiedName~Confirm|FullyQualifiedName~BackupSelection|FullyQualifiedName~Progress|FullyQualifiedName~Wpf"
+ ```
+
+ Expected: the solution builds, all selected tests pass with `Failed: 0`, and both the old WinForms app and temporary WPF app remain buildable at this point. Record the SDK version, Windows build, monitor resolution, current DPI scale, and test summary in the verification record.
+
+- [ ] **Step 3: Run the WPF workflow smoke on a real Windows desktop before any WinForms deletion**
+
+ Launch the temporary WPF shell from an elevated Windows test account or a disposable Windows VM:
+
+ ```powershell
+ dotnet run --project src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj -c Debug
+ ```
+
+ Use a disposable backup root and reversible test data. Do not initiate a real registry restore on a personal workstation. When a restore write is required for the smoke, use the disposable VM, select one reversible whole module from a snapshot created in that VM, and obtain the VM operator's confirmation immediately before clicking **Start restore**.
+
+ Record each result in the verification document:
+
+ | Scenario | Exact interaction | Expected observable result |
+ | --- | --- | --- |
+ | Startup | Start the command above and wait for the window to settle. | The WPF `MainWindow` opens on Timeline with compact top bar, wordmark, Settings, and Create snapshot; no WinForms rail, dashboard home, ownerless dialog, or startup exception appears. |
+ | Timeline source states | Load one verified snapshot, one partial snapshot, a catalog session failure created through `RecordSessionFailure`, and one malformed/unreadable fixture. | All four states show honest labels and diagnostics. Only Verified/Partial can enter Compare; Failed/Unreadable have no restore action. |
+ | Timeline selection | Select a verified compressed snapshot, enter Compare, return, then select another snapshot. | Preparation is read-only; changing source clears a non-empty restore set only after its explicit confirmation; temporary payload scope is released when leaving Compare. |
+ | Compare | Let comparison load, use All and Changed-only filters, select a row, open its detail tray, cancel an in-flight comparison, and load it again. | Rows remain catalog-ordered; Changed, Same, Unavailable, and Not captured are distinguishable with text/icons; a one-module error does not erase other rows; cancel is awaited and leaves no leaked temporary payload. |
+ | Confirm | Add restorable modules, enter Confirm, inspect impacts, cancel, then repeat in the disposable VM and accept the existing consent/snapshot gates. | Selection is whole-module; exact existing process closures, Explorer restart, and sign-out impacts are shown; no reboot impact is invented; cancel writes nothing; accepted execution follows existing `RestorePlan`, `SnapshotGate`, `RestoreScope`, and `RestoreDispatch`. |
+ | Create snapshot | Choose an existing scope/preset and compression option, start one disposable backup, observe progress, then return to Timeline. | Run admission, progress, logs, cancellation controls, compression, manifest/log output, and late-cancel wording match Application contracts. A verified completion becomes selectable; untrusted/failed outcomes are truthful session diagnostics only. |
+ | App restore | Enter the app-restore module from the disposable snapshot and close its WPF dialog without installing software. | The WPF-owned dialog is modal to the main window and its source/list/error text matches the Application service; no WinForms form opens. |
+ | Advanced destinations | Open Advanced History, Settings, and About, then return to Timeline. | Advanced History is the same event source; theme and version settings persist correctly; About update/link flows are WPF-owned. |
+
+- [ ] **Step 4: Verify keyboard-only and UI Automation behavior on the live desktop**
+
+ Start the Windows SDK inspection utility while the WPF shell is visible:
+
+ ```powershell
+ $inspect = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter Inspect.exe | Select-Object -First 1 -ExpandProperty FullName
+ if ([string]::IsNullOrWhiteSpace($inspect)) { throw "Install the Windows SDK Inspect.exe tool before recording UI Automation evidence." }
+ & $inspect
+ ```
+
+ Then execute and record this matrix. For each row record the UIA Name, ControlType, IsEnabled state, and keyboard result observed with Inspect.exe.
+
+ | Keyboard path | Expected result |
+ | --- | --- |
+ | Tab from the top bar through Create snapshot, Timeline, filters, row actions, restore-set actions, Confirm, Settings, and About | Visible focus never disappears, no focus trap occurs, and labels describe each action. |
+ | Left/Right while `TimelineEventList` has focus | Moves among timeline events without a mouse. |
+ | Enter on Verified or Partial event | Selects the event and opens Compare. |
+ | Enter on Failed or Unreadable event | Opens diagnostic detail only; no restore set or restore command becomes available. |
+ | Tab/Shift+Tab and Space/Enter in Compare and Confirm | Reaches filters, row selection, detail tray, add/remove actions, cancel, and confirm in a predictable order. |
+ | Narrow Timeline list fallback | Exposes the same title, timestamp, state, diagnostic, enabled/disabled status, and selection action as the visual timeline. |
+ | WPF dialog open | Dialog has the main WPF window as owner, remains in the foreground, and its controls are independently discoverable by UIA. |
+
+ Use the Task 1 automation identifiers as the stable evidence points. For a row with an error, verify the live status is announced politely rather than converted to “nothing found.”
+
+- [ ] **Step 5: Verify Light, Dark, Follow system, contrast, and reduced-motion behavior**
+
+ In the WPF Settings view, select **Light**, **Dark**, and **Follow system** in separate runs. For Follow system, change the Windows app color preference and return focus to the application. Test reduced motion on the disposable test profile only:
+
+ ```powershell
+ Start-Process "ms-settings:easeofaccess-display"
+ ```
+
+ Capture the previous animation setting, turn off **Animation effects**, restart the WPF app, select a Timeline event, filter Compare, and open/close the detail tray. Restore the prior Windows setting after the test.
+
+ Expected:
+
+ - Light and Dark use neutral Windows surfaces, mineral-blue primary action, restrained coral warnings, Segoe UI Variable for normal UI text, and the packaged monospace face only for logs/technical values.
+ - Status is never encoded only by color; focus, selected, warning, unavailable, and changed states remain distinguishable in grayscale.
+ - Inspect foreground/background pairs with a contrast checker and record a contrast ratio of at least `4.5:1` for every normal-text token in Light and Dark.
+ - Follow system changes only while Follow system is selected; explicit Light and Dark do not react to the OS setting.
+ - With Animation effects disabled, nonessential selection/filter/tray motion is absent and state changes remain immediate and usable.
+
+- [ ] **Step 6: Verify all required DPI scales and the minimum-width layout on the live desktop**
+
+ Open Windows Display Settings on the disposable test profile:
+
+ ```powershell
+ Start-Process "ms-settings:display"
+ ```
+
+ Restart the WPF app at 100%, 125%, 150%, 175%, and 200% scaling. At each scale inspect Timeline, Compare, Confirm, backup selection, progress, result, settings, About, and a dialog. At 100%, resize the window to its 1024 px minimum usable width and test the Compare workspace.
+
+ Expected:
+
+ - No clipped labels, overlap, unusable hit targets, hidden keyboard focus, or horizontal content loss occurs at any required scale.
+ - The window cannot be reduced below its usable 1024 px minimum.
+ - At the minimum width, comparison evidence and restore-set panes stack vertically; both remain reachable through keyboard and UIA.
+ - At wider widths the normal two-pane comparison presentation returns without duplicate content or a stale selection.
+
+- [ ] **Step 7: Capture, compare, and commit deterministic screenshots**
+
+ Use the same test account, 100% scaling unless the filename names another scale, a fixed application window size, the same disposable fixture root, and no unrelated windows. Capture the exact filenames listed in **Files** for this task. For each baseline, record theme, DPI, logical window size, fixture state, and SHA-256 in the verification document.
+
+ Compare every new capture side by side with its committed predecessor when a predecessor exists. A change is accepted only when it matches the approved Timeline + Compare visual direction: no permanent sidebar, generic dashboard card grid, giant failure headline, glow, purple gradient, decorative chart, or color-only state. Record `new baseline` for the first approved set and `matched` or a concise visual difference for later runs.
+
+ Expected screenshot coverage:
+
+ - Light and Dark Timeline screenshots cover normal, Partial, Failed, and Unreadable source states.
+ - Light and Dark Compare screenshots show All modules, including Changed, Same, Unavailable, and Not captured evidence.
+ - Light and Dark Confirm screenshots show grouped existing restore impacts and the explicit start action.
+ - Timeline, Compare, and Confirm each have 100%, 150%, and 200% baseline coverage.
+ - `compare-all-light-1024w.png` demonstrates the required stacked narrow layout.
+
+ Before setting the gate result, verify the baseline directory has exactly the required files and emit their hashes for the record:
+
+ ```powershell
+ $baselineRoot = 'docs\superpowers\verification\2026-08-09-wpf-cutover\screenshots'
+ $expectedBaselineNames = @(
+ 'timeline-normal-light-100.png', 'timeline-normal-dark-100.png',
+ 'timeline-partial-light-100.png', 'timeline-partial-dark-100.png',
+ 'timeline-failed-light-100.png', 'timeline-failed-dark-100.png',
+ 'timeline-unreadable-light-100.png', 'timeline-unreadable-dark-100.png',
+ 'compare-all-light-100.png', 'compare-all-dark-100.png',
+ 'confirm-light-100.png', 'confirm-dark-100.png',
+ 'timeline-normal-light-150.png', 'compare-all-light-150.png',
+ 'confirm-light-150.png', 'timeline-normal-light-200.png',
+ 'compare-all-light-200.png', 'confirm-light-200.png',
+ 'compare-all-light-1024w.png'
+ ) | Sort-Object
+ $actualBaselineNames = @(Get-ChildItem $baselineRoot -File -Filter '*.png' |
+ Select-Object -ExpandProperty Name | Sort-Object)
+ $baselineDelta = Compare-Object $expectedBaselineNames $actualBaselineNames
+ if ($baselineDelta) {
+ throw "Screenshot baseline set differs from the required 19 files:`n$($baselineDelta | Out-String)"
+ }
+ Get-ChildItem $baselineRoot -File -Filter '*.png' |
+ Sort-Object Name | Get-FileHash -Algorithm SHA256
+ ```
+
+ Expected: the command prints exactly 19 named SHA-256 rows and throws if any capture is absent, extra, or misspelled.
+
+- [ ] **Step 8: Make the deletion decision explicit**
+
+ In the verification record, set `Result: PASS — real Windows desktop gate complete` only when every Task 2 row passed, every listed screenshot exists, and the desktop exercise observed an actual WPF startup, Timeline, Compare, Confirm, backup/progress/results, dialogs, theme modes, keyboard/UIA, reduced motion, DPI, and minimum-width layout.
+
+ If any item fails, set `Result: BLOCKED —` followed by the exact scenario name and observed failure, repair the owning WPF view/view-model/service, then repeat the affected focused test and live desktop row. Do not begin Task 3 or remove a WinForms file while the result is blocked.
+
+- [ ] **Step 9: Commit the real-desktop evidence and baselines**
+
+ ```powershell
+ git add docs\superpowers\verification\2026-08-09-wpf-cutover.md docs\superpowers\verification\2026-08-09-wpf-cutover\screenshots src\WinRestoreKit.Wpf\Views\TimelineView.xaml src\WinRestoreKit.Wpf\Views\ComparisonWorkspaceView.xaml src\WinRestoreKit.Wpf\Views\ConfirmView.xaml src\WinRestoreKit.Wpf\Views\SettingsView.xaml
+ git commit -m "docs: record WPF desktop verification baselines"
+ ```
+
+### Task 3: Atomically transfer the `WinRestoreKit` shipping identity to WPF and remove every obsolete WinForms artifact
+
+**Files:**
+- Modify: `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj`
+- Move: `src/WinRestoreKit/app.manifest` to `src/WinRestoreKit.Wpf/app.manifest`
+- Move: `src/WinRestoreKit/WinRestoreKit.ico` to `src/WinRestoreKit.Wpf/WinRestoreKit.ico`
+- Move: `src/WinRestoreKit/Fonts/IBMPlexMono-Regular.ttf` to `src/WinRestoreKit.Wpf/Assets/Fonts/IBMPlexMono-Regular.ttf`
+- Move: `src/WinRestoreKit/Fonts/IBMPlexMono-Medium.ttf` to `src/WinRestoreKit.Wpf/Assets/Fonts/IBMPlexMono-Medium.ttf`
+- Delete: `src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs`
+- Modify: `src/WinRestoreKit.Core/Conf/AppStoreApps.cs`
+- Modify: `src/WinRestoreKit/Properties/AssemblyInfo.cs`
+- Modify: `src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj`
+- Create: `src/WinRestoreKit.Tests/ShippingIdentityTests.cs`
+- Modify: `src/WinRestoreKit.Tests/VersionParsingTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RebrandIdentityTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RestoreDeclarationTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RunSummaryTests.cs`
+- Modify: `src/WinRestoreKit.Tests/AssemblyInfo.cs`
+- Modify: `src/WinRestoreKit.Tests/UpdateCheckVerdictTests.cs`
+- Modify: `src/WinRestoreKit.Tests/OsVersionTests.cs`
+- Modify: `src/WinRestoreKit.Tests/AppRestoreDialogTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupFoldersReadTests.cs`
+- Modify: `src/WinRestoreKit.Tests/LogHelperTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ScopeGroupsPrivacyTests.cs`
+- Create: `src/WinRestoreKit.Tests/AppRestoreOwnerTests.cs` (rewrite/rename of `RestoreDialogOwnerTests.cs`)
+- Modify: `src/WinRestoreKit.Tests/ArchiveProgressTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupDestinationContainmentTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupPresetsTests.cs`
+- Modify: `src/WinRestoreKit.Tests/LockedPayloadBackupTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ModuleShapeTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RestoreConsentCancellationTests.cs`
+- Modify: `src/WinRestoreKit.Tests/SnapshotFolderPathTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ViewDataHelperTests.cs`
+- Modify: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+- Modify: `src/WinRestoreKit.sln`
+- Delete: `src/WinRestoreKit/WinRestoreKit.csproj`
+- Delete: `src/WinRestoreKit/Program.cs`
+- Delete: `src/WinRestoreKit/MainForm.cs`
+- Delete: `src/WinRestoreKit/MainForm.Designer.cs`
+- Delete: `src/WinRestoreKit/MainForm.resx`
+- Delete: `src/WinRestoreKit/GitHub.cs`
+- Delete: `src/WinRestoreKit/GitHubIcon.png`
+- Delete: `src/WinRestoreKit/Views/`
+- Delete: `src/WinRestoreKit/Forms/`
+- Delete: `src/WinRestoreKit/Controls/`
+- Delete: `src/WinRestoreKit/Helpers/`
+- Delete: `src/WinRestoreKit/Orchestration/`
+- Delete: `src/WinRestoreKit/Results/`
+- Delete: `src/WinRestoreKit/Fonts/Barlow-Regular.otf`
+- Delete: `src/WinRestoreKit/Fonts/Barlow-Medium.otf`
+- Delete: `src/WinRestoreKit/Fonts/Barlow-SemiBold.otf`
+- Delete: `src/WinRestoreKit/Fonts/BarlowCondensed-Regular.otf`
+- Delete: `src/WinRestoreKit/Fonts/BarlowCondensed-SemiBold.otf`
+- Delete: `src/WinRestoreKit/Fonts/BarlowCondensed-Bold.otf`
+- Delete: `src/WinRestoreKit/Properties/Resources.resx`
+- Delete: `src/WinRestoreKit/Properties/Resources.Designer.cs`
+- Delete: `src/WinRestoreKit.Tests/BackupPageViewTests.cs`
+- Delete: `src/WinRestoreKit.Tests/HomePageViewBaselineTests.cs`
+- Delete: `src/WinRestoreKit.Tests/HomePageViewDriftTests.cs`
+- Delete: `src/WinRestoreKit.Tests/MainFormRunAdmissionTests.cs`
+- Delete: `src/WinRestoreKit.Tests/NavigationServiceTests.cs`
+- Delete: `src/WinRestoreKit.Tests/ProgressPageViewTests.cs`
+- Delete: `src/WinRestoreKit.Tests/HistoryPageViewTests.cs`
+- Delete: `src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs`
+- Delete: `src/WinRestoreKit.Tests/ShellLayoutTests.cs`
+- Delete: `src/WinRestoreKit.Tests/FontLoaderTests.cs`
+- Delete: `src/WinRestoreKit.Tests/CompressedDriftPayloadTests.cs`
+- Test: `src/WinRestoreKit.Tests/ShippingIdentityTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+- Test: `src/WinRestoreKit.Tests/AdvancedHistoryViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/BackupWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ProgressWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ResultWorkspaceViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/RestoreConsentDialogTests.cs`
+- Test: `src/WinRestoreKit.Tests/AppRestoreDialogTests.cs`
+- Test: `src/WinRestoreKit.Tests/AppRestoreOwnerTests.cs`
+
+**Interfaces:**
+- Consumes: Application implementations of orchestration, run state, run summary, backup root, scope groups, presets, folders, update checking, and app-restore parsing; WPF adapters and view models from the four upstream plans; Core's existing `BackupBase`, payload, manifest, restore, and logging contracts; the Task 2 desktop gate.
+- Produces: the only `WinRestoreKit` executable/assembly is WPF; the solution contains `Core`, `Application`, `Wpf`, and `Tests`; the physical raw version source survives at its exact old path; no old WinForms project, source, resource, helper, control, form, test, or compatibility path remains.
+
+- [ ] **Step 1: Re-run the real-desktop deletion gate and stop on missing evidence**
+
+ Before writing an identity test or issuing any removal command, read `docs/superpowers/verification/2026-08-09-wpf-cutover.md` and verify that `Result` is exactly `PASS — real Windows desktop gate complete` and every screenshot listed in Task 2 exists.
+
+ Run:
+
+ ```powershell
+ Test-Path docs\superpowers\verification\2026-08-09-wpf-cutover.md
+ Get-ChildItem docs\superpowers\verification\2026-08-09-wpf-cutover\screenshots -File | Measure-Object
+ ```
+
+ Expected: the report exists and the screenshot file count is `19`. If either condition is false, stop. WinForms deletion is not authorized until the real desktop evidence is complete.
+
+- [ ] **Step 2: Write and observe the failing final-identity regression test while temporary side-by-side references still compile**
+
+ Add `ShippingIdentityTests.cs`. During this one red test cycle, WPF still has its temporary assembly name and the test project still has both application references, so the test can compile and fail only on the intended shipping-identity assertion.
+
+ ```csharp
+ using System.IO;
+ using System.Linq;
+ using System.Reflection;
+ using System.Runtime.Versioning;
+ using DataHelper;
+ using WinRestoreKit.Wpf;
+ using Xunit;
+
+ namespace WinRestoreKit.Tests
+ {
+ public sealed class ShippingIdentityTests
+ {
+ [Fact]
+ public void ShippingWpfAssembly_UsesTheRawFallbackVersionSource()
+ {
+ string source = File.ReadAllText(Path.Combine(
+ AppContext.BaseDirectory, "TestData", "AssemblyInfo.cs"));
+ Assembly assembly = typeof(App).Assembly;
+ string compiled = assembly.GetCustomAttribute().Version;
+
+ Assert.Equal("WinRestoreKit", assembly.GetName().Name);
+ Assert.Equal(Data.ParseLatestVersion(source), compiled);
+ Assert.Equal(3, compiled.Split('.').Length);
+ }
+
+ [Fact]
+ public void ShippingWpfAssembly_HasNoWinFormsAssemblyReference()
+ {
+ string[] references = typeof(App).Assembly
+ .GetReferencedAssemblies()
+ .Select(reference => reference.Name)
+ .ToArray();
+
+ Assert.DoesNotContain("System.Windows.Forms", references);
+ Assert.Contains(typeof(App).Assembly.GetCustomAttributes(),
+ attribute => attribute.PlatformName == "windows7.0");
+ }
+ }
+ }
+ ```
+
+- [ ] **Step 3: Run the identity test to verify it fails for the temporary WPF identity**
+
+ Run:
+
+ ```powershell
+ dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj -c Debug --filter FullyQualifiedName~ShippingIdentityTests
+ ```
+
+ Expected: `ShippingWpfAssembly_UsesTheRawFallbackVersionSource` fails because the temporary WPF assembly name is `WinRestoreKit.Wpf`. Do not commit this red intermediate state.
+
+- [ ] **Step 4: Preserve every behavioral test before removing its WinForms host**
+
+ Verify this ownership map and make the stated test migration before deleting the old source:
+
+ | Legacy path or test | Required surviving owner and assertion | Cutover action |
+ | --- | --- | --- |
+ | `Orchestration/BackupRestoreOrchestrator.cs`, `RunControl.cs`, `RunCoordinator.cs`, `IRunUi.cs`, `Results/RunSummary.cs` | `src/WinRestoreKit.Application/`; `RunCoordinatorTests`, `RunControlTests`, `RunSummaryTests`, backup/restore lifecycle tests compile without WinForms imports. | Remove old files after tests target Application. |
+ | `Helpers/BackupRootRegistry.cs` | `Application/Settings/BackupRootRegistry.cs`; registry format and destination tests retain exact behavior. | Remove old file. |
+ | `Helpers/UpdateCheck.cs` | `Application/Updates/VersionInfo.cs`, `UpdateVerdict.cs`, `UpdateCheckService.cs`, plus WPF presenters. | Retarget version tests; keep raw URL/path invariant. |
+ | `Program` version/update helpers and their tests | Application `VersionInfo`, `UpdateVerdict`, and `UpdateCheckService`; final `App` assembly. | Retarget `VersionParsingTests`, `UpdateCheckVerdictTests`, `RebrandIdentityTests`, `RestoreDeclarationTests`, and `OsVersionTests` from `Program`/`MainForm`; preserve unknown-version, normalization, raw-fallback, startup-failure wording, and product-identity assertions. Remove every stale WinForms/startup comment. |
+ | `Helpers/RichTextBoxLogSink.cs`, `LogHelperTargetExtensions`, and `LogHelperTests.cs` | Core `ILogSink` and `LogHelper.SetSink(ILogSink)` with WPF `WpfLogSink`. | Replace the `RichTextBox` construction with a recording `ILogSink` fake; retain braces, format, clear, and no-sink behavior, then remove every `SetTarget` call and extension. |
+ | `Views/ScopeGroups.cs`, `BackupPresets.cs`, `BackupFolders.cs`, `WatchedGroups.cs` | Application data contracts plus WPF backup/history view models. | Retarget `BackupPresetsTests`, `BackupFoldersReadTests`, `ScopeGroupsPrivacyTests`, and `ViewDataHelperTests`; retain exact membership literals, privacy-safe exclusions, and folder behavior. |
+ | `Forms/RestAppsForm.cs` and its former parser/list/install helpers | `Application/AppRestore/AppRestoreService.cs` and `WpfAppRestoreDialog`. | Rewrite `AppRestoreDialogTests.cs` to use `AppRestoreService.BuildSources`, `ReadFromSource`, `ComposeListState`, `RouteProblem`, and `InstallAsync`; preserve exact export-source precedence, parser states, package IDs, winget outcome, and failure wording without a `Views` or WinForms import. |
+ | `HomePageView` drift checks and restore wizard artifact discovery | `SnapshotComparisonService`, `SnapshotPayloadPreparationService`, Timeline/Compare view models. | Move the old drift and compressed-payload assertions into `SnapshotComparisonServiceTests.cs` and `SnapshotPayloadPreparationServiceTests.cs`; no reflection over `HomePageView` remains. |
+
+ Write the `AppRestoreDialogTests.cs` red assertions against the Application service before removing the Form helpers, then make the test green only after the helper migration:
+
+ ```csharp
+ AppExport export = AppRestoreService.ReadFromSource(sourcePath);
+
+ Assert.Equal(AppExportState.Ok, export.State);
+ Assert.Equal(new[] { "Microsoft.PowerToys" }, export.PackageIdentifiers);
+ ```
+
+ `ReadFromSource` must use `AppStoreApps.ExportPathIn` and dispose its private payload `ReadScope`; do not recreate JSON parsing in WPF or in the test.
+
+ Rewrite `LogHelperTests.cs` before deleting `Helpers/RichTextBoxLogSink.cs`:
+
+ ```csharp
+ private sealed class RecordingLogSink : ILogSink
+ {
+ internal List Entries { get; } = new List();
+
+ public void Append(string text) => Entries.Add(text);
+ public void Clear() => Entries.Clear();
+ }
+
+ [Fact]
+ public void LogMessage_UnmatchedBrace_DoesNotThrowAndStillLogs()
+ {
+ RecordingLogSink sink = new RecordingLogSink();
+ LogHelper.Instance.SetSink(sink);
+ try
+ {
+ LogHelper.Instance.LogMessage("failed on {0 unbalanced");
+ Assert.Contains("unbalanced", sink.Entries[0]);
+ }
+ finally
+ {
+ LogHelper.Instance.SetSink(null);
+ }
+ }
+ ```
+
+ Preserve the existing `Log`, `LogMessage`, `ClearLog`, and no-sink assertions; no test constructs a UI control after this conversion.
+
+ Update the test doubles in `ArchiveProgressTests.cs`, `BackupDestinationContainmentTests.cs`, `BackupDestinationLifecycleTests.cs`, `LockedPayloadBackupTests.cs`, `RestoreConsentCancellationTests.cs`, and `SnapshotFolderPathTests.cs` to remove `using System.Windows.Forms;` and a typed `IWin32Window Owner`. They implement `IRunUi.DialogOwner` with an opaque sentinel; tests for app restore live in `AppRestoreOwnerTests.cs`, not generic run doubles.
+
+ ```csharp
+ private sealed class TestRunUi : IRunUi
+ {
+ private static readonly object DialogOwnerSentinel = new object();
+
+ internal List ProgressTexts { get; } = new List();
+
+ public object DialogOwner => DialogOwnerSentinel;
+ public void SetProgressText(string text) => ProgressTexts.Add(text ?? string.Empty);
+ public void SetProgressPercent(int percent) { }
+ public void SetProgressDetail(string groupInfo, string elapsed, string remaining,
+ string throughput, long bytesWritten, int errors, int warnings) { }
+ public void ShowSummary(RunSummary summary, string caption,
+ IReadOnlyList outcomes) { }
+ public IReadOnlyList ShowConsentDialog(RestorePlan plan) => Array.Empty();
+ public bool ConfirmSnapshotOverride(string text, string caption) => false;
+ public void ShowPlanCompositionError(string text, string caption) { }
+ public void SetExplorerRestartVisible(bool visible) { }
+ }
+ ```
+
+ Preserve the Core API and outcome semantics in `src/WinRestoreKit.Core/Conf/AppStoreApps.cs`: `RestoreAsync(string path, object owner)`, `Restore(string path, object owner)`, and `Action RestoreDialog` remain unchanged. Rewrite only stale `RestAppsForm`, WinForms, `IWin32Window`, and `Program.Main` XML remarks so they describe `WpfAppRestoreDialog`, an opaque shell-owned STA `Window`, and `App.OnStartup`; do not change visibility, failure messages, skipped result, or callback invocation. `AppRestoreOwnerTests.cs` must prove all three paths:
+
+ ```csharp
+ using Conf;
+ using Xunit;
+
+ [Fact]
+ public async Task RestoreAsync_ForwardsTheOpaqueDialogOwner()
+ {
+ object expectedOwner = new object();
+ object actualOwner = null;
+ AppStoreApps.RestoreDialog = (_, owner) => actualOwner = owner;
+
+ ModuleResult result = await new AppStoreApps().RestoreAsync("apps.json", expectedOwner);
+
+ Assert.Same(expectedOwner, actualOwner);
+ Assert.Contains(result.Steps, step => step.State == ResultState.Skipped);
+ }
+ ```
+
+ Keep distinct facts named `Restore_WithNoDialogRegistered_FailsRatherThanClaimingSkipped`, `Restore_WithADialogButNoOwner_FailsRatherThanClaimingSkipped`, `Restore_WithADialogRegistered_OpensItForTheSelectedSourceAndReportsSkipped`, and `RestoreAsync_RunsTheDialogOnTheCallersThread`; all use an `object` sentinel rather than a WinForms type. Move the old static `DialogHook` into this renamed file and use it to restore the prior delegate after every test.
+
+ Reset the mutable static delegate in `finally`/`IDisposable` cleanup. Keep the module's existing backup/result behavior unchanged: the moved Application orchestrator invokes exactly `await appStoreApps.RestoreAsync(currentRestorePath, ui.DialogOwner)`, and the WPF composition root supplies the current shell `Window` as that opaque owner.
+
+- [ ] **Step 5: Perform the identity transfer, test-project migration, solution removal, asset moves, and source deletion as one atomic green change**
+
+ Do not create two app assemblies named `WinRestoreKit` in a green tree. First remove the old project from the solution, then make the WPF assembly the shipping identity while removing the old app source and all tests that require it.
+
+ ```powershell
+ dotnet sln src\WinRestoreKit.sln remove src\WinRestoreKit\WinRestoreKit.csproj
+ New-Item -ItemType Directory -Force src\WinRestoreKit.Wpf\Assets\Fonts | Out-Null
+ git mv src\WinRestoreKit\app.manifest src\WinRestoreKit.Wpf\app.manifest
+ git mv src\WinRestoreKit\WinRestoreKit.ico src\WinRestoreKit.Wpf\WinRestoreKit.ico
+ git mv src\WinRestoreKit\Fonts\IBMPlexMono-Regular.ttf src\WinRestoreKit.Wpf\Assets\Fonts\IBMPlexMono-Regular.ttf
+ git mv src\WinRestoreKit\Fonts\IBMPlexMono-Medium.ttf src\WinRestoreKit.Wpf\Assets\Fonts\IBMPlexMono-Medium.ttf
+ ```
+
+ Change `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj` exactly as follows. This is the only app project with `GenerateAssemblyInfo=false`.
+
+ ```xml
+
+
+ WinExe
+ net8.0-windows
+ true
+ WinRestoreKit.Wpf
+ WinRestoreKit
+ app.manifest
+ WinRestoreKit.ico
+ false
+ disable
+ disable
+ AnyCPU
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ Do not set `Version`, `AssemblyVersion`, `FileVersion`, `InformationalVersion`, `PublishTrimmed`, or `UseWindowsForms` in this project. Delete `src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs`, because the linked physical file becomes the one source of WPF assembly attributes and test friendship.
+
+ Retain these manifest elements exactly after the move; remove only WinForms-specific explanatory comments:
+
+ ```xml
+
+ true
+ ```
+
+ Do not add a `dpiAware` element. WPF handles DPI without `Application.SetHighDpiMode`.
+
+ In `WinRestoreKit.Tests.csproj`, remove the old WinForms project reference and replace the temporary dual-framework settings with final WPF-only support:
+
+ ```xml
+
+ net8.0-windows
+ true
+ false
+ true
+ disable
+ disable
+ AnyCPU
+
+
+
+
+
+
+ ```
+
+ Keep the existing `None Include="..\WinRestoreKit\Properties\AssemblyInfo.cs"` test-data link unchanged.
+
+ Update the comments in the physical `src/WinRestoreKit/Properties/AssemblyInfo.cs` from WinForms to WPF but keep the physical path, attribute order, and exact version source format unchanged:
+
+ ```csharp
+ [assembly: SupportedOSPlatform("windows7.0")]
+ [assembly: AssemblyTitle("WinRestoreKit")]
+ [assembly: AssemblyVersion("0.0.1")]
+ [assembly: AssemblyFileVersion("0.0.1")]
+ [assembly: InternalsVisibleTo("WinRestoreKit.Tests")]
+ ```
+
+ Keep `Data.Uri.URL_ASSEMBLY` exactly `https://raw.githubusercontent.com/nicolasestrem/WinRestoreKit/main/src/WinRestoreKit/Properties/AssemblyInfo.cs`.
+
+- [ ] **Step 6: Delete every obsolete production and construction-test path without a shim**
+
+ Remove the old shell in the same atomic change. Retain only `src/WinRestoreKit/Properties/AssemblyInfo.cs` under the old directory.
+
+ ```powershell
+ git rm src\WinRestoreKit\WinRestoreKit.csproj src\WinRestoreKit\Program.cs src\WinRestoreKit\MainForm.cs src\WinRestoreKit\MainForm.Designer.cs src\WinRestoreKit\MainForm.resx src\WinRestoreKit\GitHub.cs src\WinRestoreKit\GitHubIcon.png
+ git rm -r src\WinRestoreKit\Views src\WinRestoreKit\Forms src\WinRestoreKit\Controls src\WinRestoreKit\Helpers src\WinRestoreKit\Orchestration src\WinRestoreKit\Results
+ git rm src\WinRestoreKit\Fonts\Barlow-Regular.otf src\WinRestoreKit\Fonts\Barlow-Medium.otf src\WinRestoreKit\Fonts\Barlow-SemiBold.otf src\WinRestoreKit\Fonts\BarlowCondensed-Regular.otf src\WinRestoreKit\Fonts\BarlowCondensed-SemiBold.otf src\WinRestoreKit\Fonts\BarlowCondensed-Bold.otf
+ git rm src\WinRestoreKit\Properties\Resources.resx src\WinRestoreKit\Properties\Resources.Designer.cs
+ git rm src\WinRestoreKit.Tests\BackupPageViewTests.cs src\WinRestoreKit.Tests\HomePageViewBaselineTests.cs src\WinRestoreKit.Tests\HomePageViewDriftTests.cs src\WinRestoreKit.Tests\MainFormRunAdmissionTests.cs src\WinRestoreKit.Tests\NavigationServiceTests.cs src\WinRestoreKit.Tests\ProgressPageViewTests.cs src\WinRestoreKit.Tests\HistoryPageViewTests.cs src\WinRestoreKit.Tests\RestoreDialogOwnerTests.cs src\WinRestoreKit.Tests\ShellLayoutTests.cs src\WinRestoreKit.Tests\FontLoaderTests.cs src\WinRestoreKit.Tests\CompressedDriftPayloadTests.cs
+ ```
+
+ Apply this exact test disposition before the delete command:
+
+ | Legacy test | Required replacement or retained assertion |
+ | --- | --- |
+ | `BackupPageViewTests.cs` | `BackupWorkspaceViewModelTests.cs` verifies selected scopes/modules, validation, compression, and request emission. |
+ | `ProgressPageViewTests.cs` | `ProgressWorkspaceViewModelTests.cs` and `ResultWorkspaceViewModelTests.cs` verify cancellation state, progress text, late-cancel summary, and outcomes. |
+ | `MainFormRunAdmissionTests.cs` | `RunCoordinatorTests.cs` plus WPF progress navigation/runtime test verify one active run and replacement only after completion. |
+ | `NavigationServiceTests.cs` | `ShellViewModel`/runtime tests verify explicit workspace navigation. |
+ | `ShellLayoutTests.cs` | Task 1 automation and Task 2 DPI/screenshot evidence verify actual WPF layout. |
+ | `HomePageViewBaselineTests.cs` | `TimelineViewModelTests.cs`, `TimelineAccessibilityTests.cs`, and Task 2 state baselines verify empty/verified/failed visuals. |
+ | `HomePageViewDriftTests.cs` | `SnapshotComparisonServiceTests.cs` verifies changed/same/unavailable evidence. |
+ | `CompressedDriftPayloadTests.cs` | `SnapshotComparisonServiceTests.cs` and `SnapshotPayloadPreparationServiceTests.cs` retain compressed-payload evidence. |
+ | `HistoryPageViewTests.cs` | `AdvancedHistoryViewModelTests.cs` and `SnapshotEventCatalogTests.cs` cover event reading, filtering, pruning rules, and source parity. |
+ | `RestoreDialogOwnerTests.cs` | Rename and rewrite as `AppRestoreOwnerTests.cs`; Core unit tests prove opaque-owner forwarding plus missing-owner/missing-dialog outcomes, while WPF STA runtime tests verify a current WPF `Window` reaches the dialog. |
+ | `FontLoaderTests.cs` | Task 1 runtime tests and Task 2 baselines verify packaged IBM Plex Mono; normal UI uses Segoe UI Variable. |
+ | `AppRestoreDialogTests.cs` / `ModuleShapeTests.cs` form calls | `AppRestoreService` tests retain parser, source, winget outcome, and failure wording; replace `RestAppsForm.Describe` assertions with `AppRestoreService.RouteProblem`; WPF STA dialog tests retain ownership. |
+ | `BackupPresetsTests.cs`, `BackupFoldersReadTests.cs`, `ScopeGroupsPrivacyTests.cs`, and `ViewDataHelperTests.cs` | Retarget Application scope/preset/folder types and retain exact membership literals, source ordering, and privacy exclusions. |
+
+ `GitHub.cs`/`Stargazers` has no consumers; remove it without replacement. Do not preserve a `MainForm`, `RestAppsForm`, `RestoreConfirmForm`, `NavigationService`, `RichTextBoxLogSink`, `ProgressLogSink`, `MessageBoxIcon`, or `IWin32Window` compatibility alias. The pre-existing Core `object DialogOwner`/`RestoreDialog` seam is not a compatibility alias and must remain opaque to Application.
+
+- [ ] **Step 7: Retarget the surviving version, summary, declaration, and test-assembly assertions**
+
+ Replace direct `MainForm`/`Program` references in version tests with the final WPF assembly and the Foundation `VersionInfo` normalizer. Preserve all three-part and malformed-input assertions.
+
+ ```csharp
+ Assembly shipping = typeof(global::WinRestoreKit.Wpf.App).Assembly;
+ string compiled = shipping
+ .GetCustomAttribute()
+ .Version;
+ string parsed = global::DataHelper.Data.ParseLatestVersion(RealAssemblyInfoText());
+
+ Assert.Equal(parsed, compiled);
+ Assert.Equal("1.2.3", VersionInfo.Normalize(" 1.2.3+build "));
+ ```
+
+ In `RunSummaryTests.cs`, replace each `MessageBoxIcon` expectation with neutral severity:
+
+ ```csharp
+ Assert.Equal(RunSeverity.Warning, summary.Severity);
+ ```
+
+ In `RestoreDeclarationTests.cs`, use `typeof(global::WinRestoreKit.Wpf.App).Assembly` as the app assembly to prove no concrete `BackupBase` module leaked into the shell. In `AssemblyInfo.cs`, remove mutable WinForms `Theme` state from the parallelism rationale and retain the disabled-parallelization attribute for real process-wide data/registry tests.
+
+- [ ] **Step 8: Run the post-removal build, complete test suite, and no-legacy-path checks**
+
+ Run:
+
+ ```powershell
+ dotnet build src\WinRestoreKit.sln -c Debug
+ dotnet test src\WinRestoreKit.sln -c Debug --no-build
+
+ $removed = @(
+ 'src\WinRestoreKit\WinRestoreKit.csproj',
+ 'src\WinRestoreKit\Program.cs',
+ 'src\WinRestoreKit\MainForm.cs',
+ 'src\WinRestoreKit\Views',
+ 'src\WinRestoreKit\Forms',
+ 'src\WinRestoreKit\Controls',
+ 'src\WinRestoreKit\Helpers',
+ 'src\WinRestoreKit\Orchestration',
+ 'src\WinRestoreKit\Results'
+ ) | Where-Object { Test-Path $_ }
+ if ($removed) { throw "Obsolete WinForms paths remain: $($removed -join ', ')" }
+
+ $forms = git grep -n 'System\.Windows\.Forms' -- 'src/**/*.cs' 'src/**/*.csproj'
+ if ($forms) { throw "WinForms source references remain:`n$forms" }
+
+ $obsoleteShell = git grep -nE 'MainForm|RestoreConfirmForm|RestAppsForm|NavigationService|RichTextBoxLogSink|ProgressLogSink|MessageBoxIcon|LogHelperTargetExtensions|SetTarget\(' -- 'src/**/*.cs' 'src/**/*.csproj'
+ if ($obsoleteShell) { throw "Obsolete shell references remain:`n$obsoleteShell" }
+
+ $legacyViews = git grep -nE '^[[:space:]]*(using[[:space:]]+Views;|namespace[[:space:]]+Views([[:space:]]|\{))' -- 'src/**/*.cs'
+ if ($legacyViews) { throw "Removed legacy Views namespace references remain:`n$legacyViews" }
+
+ $typedOwners = git grep -n 'IWin32Window' -- 'src/WinRestoreKit.Application/**/*.cs' 'src/WinRestoreKit.Wpf/**/*.cs' 'src/WinRestoreKit.Tests/**/*.cs'
+ if ($typedOwners) { throw "Typed WinForms dialog owners remain outside historical Core comments:`n$typedOwners" }
+ ```
+
+ Expected: build succeeds, all Core/Application/WPF tests pass with `Failed: 0`, every removed path is absent, and the searches return no source hits. Historical docs may mention WinForms, but shipping source, project files, and tests may not.
+
+- [ ] **Step 9: Commit the single green atomic cutover**
+
+ ```powershell
+ git add -A src\WinRestoreKit src\WinRestoreKit.Application src\WinRestoreKit.Wpf src\WinRestoreKit.Tests src\WinRestoreKit.sln
+ git commit -m "refactor: make WPF the WinRestoreKit app"
+ ```
+### Task 4: Update active release documentation and prove the final self-contained WPF executable
+
+**Files:**
+- Modify: `CLAUDE.md`
+- Modify: `README.md`
+- Modify: `.claude/skills/release/SKILL.md`
+- Modify: `.claude/agents/windows-safety-reviewer.md`
+- Modify: `CHANGELOG.md`
+- Create: `docs/superpowers/verification/2026-08-09-wpf-release.md`
+- Test: `src/WinRestoreKit.Tests/ShippingIdentityTests.cs`
+- Test: `src/WinRestoreKit.Tests/VersionParsingTests.cs`
+- Test: `src/WinRestoreKit.Tests/RebrandIdentityTests.cs`
+
+**Interfaces:**
+- Consumes: final WPF project `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj`, the preserved raw fallback source, Foundation `VersionInfo`/`UpdateCheckService`, Core `Data.DataRootDir`, the final `App` assembly, manifest/icon/font resources, and the project-local `/release` procedure.
+- Produces: active build/release guidance that names WPF, a verified self-contained one-file executable, recorded manifest/resource/data-root/version evidence, and a release procedure that cannot target the removed project.
+
+- [ ] **Step 1: Write the failing documentation and artifact target check**
+
+ Before editing active docs, search them for the removed publish target:
+
+ ```powershell
+ git grep -nE 'src[\\/]WinRestoreKit[\\/]WinRestoreKit\.csproj|UseWindowsForms|Windows Forms desktop app' -- README.md CLAUDE.md .claude\skills\release\SKILL.md .claude\agents\windows-safety-reviewer.md
+ ```
+
+ Expected: this command returns active legacy references. Preserve historical changelog/design records as history; update only current instructions and the new changelog entry for this release.
+
+ Create `docs/superpowers/verification/2026-08-09-wpf-release.md` now with these fixed headings: `Build and tests`, `Publish directory`, `Embedded manifest`, `Clean-desktop executable smoke`, `Version coherence`, `GitHub Release`, and `Result`. Under each heading, record only the command, actual output, artifact SHA-256, screenshot-independent desktop observation, or remote value that proves its result. Leave the file uncommitted until Step 10 because it must describe the real released artifact, not a planned one.
+
+- [ ] **Step 2: Update the active project and release documentation to target WPF**
+
+ Make these exact documentation changes:
+
+ - `CLAUDE.md`: describe `WinRestoreKit.Wpf` as the only shipping app, `WinRestoreKit.Application` as framework-neutral shared orchestration, `WinRestoreKit.Core` as engine, and `WinRestoreKit.Tests` as xUnit. State that the raw fallback source physically remains `src/WinRestoreKit/Properties/AssemblyInfo.cs` and is linked by WPF. Replace WinForms output paths with `src\WinRestoreKit.Wpf\bin\Debug\net8.0-windows\` and `src\WinRestoreKit.Wpf\bin\Release\net8.0-windows\`. Explain WPF dispatcher/dialog/log adapters and retain the no-trimming, manifest, and one-file requirements.
+ - `README.md`: retain the build/test commands but replace the publish project path with the WPF project path shown below.
+ - `.claude/skills/release/SKILL.md`: retain the three-way version invariant, raw fallback URL, three-part format, PR/approval/tag/release order, and one-file flags; replace every old project publish path and WinForms-only trimming rationale with the final WPF project and its XAML/resource/runtime loading rationale.
+ - `.claude/agents/windows-safety-reviewer.md`: describe the WPF shell and Application layer while retaining the elevation, registry, process-kill, overwrite, and safety-review scope.
+ - `CHANGELOG.md`: add the approved WPF cutover release entry only when the release version is selected; describe the clean removal of WinForms and preserved backup compatibility without claiming a reboot requirement.
+
+ The publish command must be byte-for-byte identical in README and the release skill:
+
+ ```bat
+ dotnet publish src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj -c Release -r win-x64 --self-contained true ^
+ -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true ^
+ -p:EnableCompressionInSingleFile=true -p:DebugType=none -o publish
+ ```
+
+ Commit these active, version-independent instructions before opening a release branch. Do not stage `CHANGELOG.md` or the post-publication verification record in this commit.
+
+ ```powershell
+ git add CLAUDE.md README.md .claude\skills\release\SKILL.md .claude\agents\windows-safety-reviewer.md
+ git commit -m "docs: target release workflow at WPF shell"
+ ```
+
+- [ ] **Step 3: Run the final Release build, full test suite, and exact single-file publish**
+
+ Run:
+
+ ```powershell
+ dotnet build src\WinRestoreKit.sln -c Release
+ dotnet test src\WinRestoreKit.sln -c Release --no-build
+ Remove-Item -Recurse -Force publish -ErrorAction SilentlyContinue
+ dotnet publish src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=none -o publish
+ ```
+
+ Expected: Release build succeeds, tests report `Failed: 0`, and publish succeeds without setting `PublishTrimmed`.
+
+- [ ] **Step 4: Prove that publish contains exactly one realistic-size executable**
+
+ Run:
+
+ ```powershell
+ $files = @(Get-ChildItem .\publish -File)
+ if ($files.Count -ne 1 -or $files[0].Name -ne 'WinRestoreKit.exe') {
+ throw "Expected exactly one publish artifact named WinRestoreKit.exe; found: $($files.Name -join ', ')"
+ }
+ $sizeMiB = [math]::Round($files[0].Length / 1MB, 1)
+ if ($sizeMiB -lt 60 -or $sizeMiB -gt 90) {
+ throw "Expected a compressed self-contained WPF executable near 69 MiB; got $sizeMiB MiB."
+ }
+ Get-FileHash .\publish\WinRestoreKit.exe -Algorithm SHA256
+ ```
+
+ Expected: one `WinRestoreKit.exe`, approximately 69 MiB, no loose WPF native DLLs, and a recorded SHA-256. Do not put an extracted manifest or any verification file in `publish`; that would invalidate the one-file assertion.
+
+- [ ] **Step 5: Extract the embedded manifest and verify elevated/long-path behavior**
+
+ Run:
+
+ ```powershell
+ New-Item -ItemType Directory -Force release-verification | Out-Null
+ cmd /c "mt.exe -inputresource:publish\WinRestoreKit.exe;#1 -out:release-verification\WinRestoreKit.manifest.xml"
+ Select-String -Path release-verification\WinRestoreKit.manifest.xml -Pattern 'requestedExecutionLevel level="highestAvailable" uiAccess="false"',']*>true '
+ ```
+
+ Expected: `mt.exe` writes the extracted manifest outside `publish`, and both requested strings are found. The executable remains the only file in `publish`.
+
+- [ ] **Step 6: Verify real published-executable behavior on a clean Windows desktop or disposable equivalent**
+
+ Copy only `publish\WinRestoreKit.exe` to a clean disposable folder and launch it from a different current working directory:
+
+ ```powershell
+ $smokeRoot = Join-Path $env:TEMP 'WinRestoreKit-WpfReleaseSmoke'
+ Remove-Item -Recurse -Force $smokeRoot -ErrorAction SilentlyContinue
+ New-Item -ItemType Directory -Force $smokeRoot, "$smokeRoot\working" | Out-Null
+ Copy-Item .\publish\WinRestoreKit.exe "$smokeRoot\WinRestoreKit.exe"
+ $process = Start-Process "$smokeRoot\WinRestoreKit.exe" -WorkingDirectory "$smokeRoot\working" -PassThru
+ ```
+
+ On the clean desktop, verify and record all of these before closing the process:
+
+ | Check | Required evidence |
+ | --- | --- |
+ | Single-file startup | The copied exe opens without a .NET Desktop Runtime installation and without adjacent DLLs. |
+ | Elevated manifest behavior | UAC/highest-available behavior is observed on the disposable account and Task Manager shows the expected elevated state when the account can elevate. |
+ | Icon and title bar | Explorer, taskbar, and title bar display `WinRestoreKit.ico` and the WPF theme applies correctly. |
+ | Fonts | Normal UI uses Segoe UI Variable; a technical/log surface uses the packaged IBM Plex Mono face; no legacy Barlow face is required. |
+ | Data-root resolution | Create one disposable snapshot through the published UI. The new `app` data is beside `$smokeRoot\WinRestoreKit.exe`, never under `$smokeRoot\working`; Timeline can read it after restart. |
+ | WPF workflow | Startup, Timeline, Compare, Confirm cancellation, Settings theme switch, About, and an owner-bound dialog work from the published artifact. |
+
+ Stop the disposable process after recording the evidence:
+
+ ```powershell
+ Stop-Process -Id $process.Id
+ ```
+
+ Expected: no crash, no missing native resource, no framework-runtime prompt, and no data root created in the unrelated working directory.
+
+- [ ] **Step 7: Verify update-version coherence against the actual artifact and release inputs**
+
+ Read the raw source, normalize the Windows file-version resource to three parts, and compare it to the update fallback parser:
+
+ ```powershell
+ $source = Get-Content src\WinRestoreKit\Properties\AssemblyInfo.cs -Raw
+ $match = [regex]::Match($source, '\[assembly: AssemblyFileVersion\("(?\d+\.\d+\.\d+)"\)\]')
+ if (-not $match.Success) { throw 'The raw AssemblyFileVersion line is missing or malformed.' }
+ $sourceVersion = $match.Groups['v'].Value
+ $resourceVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path .\publish\WinRestoreKit.exe)).FileVersion
+ $parsedResource = [version]$resourceVersion
+ $artifactVersion = "$($parsedResource.Major).$($parsedResource.Minor).$($parsedResource.Build)"
+ if ($sourceVersion -ne $artifactVersion) {
+ throw "Artifact version $artifactVersion does not match raw fallback version $sourceVersion."
+ }
+ Write-Host "Version coherence preflight passed: $sourceVersion"
+ ```
+
+ Expected: the script prints the same three-part version embedded in the artifact and in the raw fallback source. `ShippingIdentityTests`, `VersionParsingTests`, and `RebrandIdentityTests` must already have passed before this release-only check.
+
+- [ ] **Step 8: Execute the tag and GitHub Release only after explicit release approval**
+
+ This step has external effects. First verify the working tree is clean and the release starts from current `main`; then obtain the approver's explicit release version and confirmation:
+
+ ```powershell
+ git checkout main
+ git pull
+ if (git status --porcelain) { throw "Release work requires a clean working tree." }
+ $version = Read-Host "Enter the explicitly approved three-part release version"
+ if ($version -notmatch '^\d+\.\d+\.\d+$') {
+ throw "Release version must have exactly three numeric parts."
+ }
+ $approval = Read-Host "Type RELEASE $version to authorize the release branch, tag, and GitHub Release"
+ if ($approval -cne "RELEASE $version") {
+ throw "Release approval was not supplied."
+ }
+ ```
+
+ Create the release branch, change only the two hand-maintained version attributes to `$version`, add the dated changelog heading, and rerun Steps 3 through 7 against the bumped artifact:
+
+ ```powershell
+ git checkout -b ("release/" + $version)
+ $assemblyInfo = 'src\WinRestoreKit\Properties\AssemblyInfo.cs'
+ $text = Get-Content $assemblyInfo -Raw
+ $text = [regex]::Replace($text,
+ '\[assembly: AssemblyVersion\("\d+\.\d+\.\d+"\)\]',
+ ('[assembly: AssemblyVersion("{0}")]' -f $version))
+ $text = [regex]::Replace($text,
+ '\[assembly: AssemblyFileVersion\("\d+\.\d+\.\d+"\)\]',
+ ('[assembly: AssemblyFileVersion("{0}")]' -f $version))
+ Set-Content -Path $assemblyInfo -Value $text -NoNewline -Encoding utf8
+ git diff -- src\WinRestoreKit\Properties\AssemblyInfo.cs
+ ```
+
+ Expected diff: only `AssemblyVersion` and `AssemblyFileVersion` change to the same three-part `$version`, and the raw `AssemblyFileVersion` line keeps its exact bracket/quote format. Add the dated `$version` release heading to `CHANGELOG.md`, then commit and open a PR:
+
+ ```powershell
+ git add src\WinRestoreKit\Properties\AssemblyInfo.cs CHANGELOG.md
+ git commit -m ("release: " + $version)
+ git push -u origin ("release/" + $version)
+ ```
+
+ Stop for PR review. Only after the PR is approved and merged to `main`, recreate and re-verify the final publish artifact from merged `main` before making a tag:
+
+ ```powershell
+ git checkout main
+ git pull
+ Remove-Item -Recurse -Force publish -ErrorAction SilentlyContinue
+ dotnet build src\WinRestoreKit.sln -c Release
+ dotnet test src\WinRestoreKit.sln -c Release --no-build
+ dotnet publish src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=none -o publish
+ ```
+
+ Rerun the exact single-file (Step 4), extracted-manifest (Step 5), clean-desktop (Step 6), and artifact-version (Step 7) checks against this merged-main output. Do not create the tag if any of those checks fails. Then validate `HEAD` before tagging and validate the tag after creation:
+
+ ```powershell
+ $headAssemblyInfo = git show 'HEAD:src/WinRestoreKit/Properties/AssemblyInfo.cs'
+ $headMatch = [regex]::Match($headAssemblyInfo,
+ '\[assembly: AssemblyFileVersion\("(?\d+\.\d+\.\d+)"\)\]')
+ if (-not $headMatch.Success -or $headMatch.Groups['v'].Value -ne $version) {
+ throw "Merged main does not contain the approved AssemblyFileVersion $version."
+ }
+ $headCommit = (git rev-parse HEAD).Trim()
+ git tag $version
+ $tagCommit = (git rev-parse ($version + '^{commit}')).Trim()
+ if ($tagCommit -ne $headCommit) {
+ throw "The new tag does not point at the verified merged-main commit."
+ }
+ git push origin $version
+ gh release create $version publish\WinRestoreKit.exe --title ("WinRestoreKit " + $version) --generate-notes
+ ```
+
+ Expected: the tagged file contains the exact same three-part `AssemblyFileVersion`, and the GitHub Release attaches only `WinRestoreKit.exe`. Never attach `bin\Release\net8.0-windows\WinRestoreKit.exe`.
+- [ ] **Step 9: Perform the post-publication remote checks and record release evidence**
+
+ Run after the release is published and `main` contains the version bump:
+ ```powershell
+
+ $latest = Invoke-RestMethod 'https://api.github.com/repos/nicolasestrem/WinRestoreKit/releases/latest' -Headers @{ 'User-Agent' = 'WinRestoreKit-release-verification' }
+ $raw = Invoke-WebRequest 'https://raw.githubusercontent.com/nicolasestrem/WinRestoreKit/main/src/WinRestoreKit/Properties/AssemblyInfo.cs' -UseBasicParsing
+ $rawMatch = [regex]::Match($raw.Content,
+ '\[assembly: AssemblyFileVersion\("(?\d+\.\d+\.\d+)"\)\]')
+ if (-not $rawMatch.Success) { throw 'Remote raw AssemblyFileVersion is missing or malformed.' }
+ $rawVersion = $rawMatch.Groups['v'].Value
+ if ($latest.tag_name -ne $rawVersion) {
+ throw "Latest release tag $($latest.tag_name) differs from raw source version $rawVersion."
+ }
+ $assets = @($latest.assets | Where-Object { $_.name -eq 'WinRestoreKit.exe' })
+ if ($assets.Count -ne 1 -or $latest.assets.Count -ne 1) {
+ throw "Latest release must contain exactly one WinRestoreKit.exe asset."
+ }
+ $downloaded = 'release-verification\WinRestoreKit.downloaded.exe'
+ Invoke-WebRequest $assets[0].browser_download_url -OutFile $downloaded -UseBasicParsing
+ $localHash = (Get-FileHash .\publish\WinRestoreKit.exe -Algorithm SHA256).Hash
+ $downloadedHash = (Get-FileHash $downloaded -Algorithm SHA256).Hash
+ if ($localHash -ne $downloadedHash) {
+ throw "Downloaded release hash $downloadedHash differs from verified local publish hash $localHash."
+ }
+ [pscustomobject]@{
+ ReleaseUrl = $latest.html_url
+ Tag = $latest.tag_name
+ RawVersion = $rawVersion
+ AssetSize = $assets[0].size
+ Sha256 = $downloadedHash
+ }
+ ```
+
+- [ ] **Step 10: Commit post-publication release evidence**
+
+ ```powershell
+ git add docs\superpowers\verification\2026-08-09-wpf-release.md
+ git commit -m "docs: record WPF release verification"
+ ```
+
+### Task 5: Perform the final regression and safety review before declaring the migration complete
+
+**Files:**
+- Modify: `docs/superpowers/verification/2026-08-09-wpf-cutover.md`
+- Modify: `docs/superpowers/verification/2026-08-09-wpf-release.md`
+- Test: `src/WinRestoreKit.Tests/ShippingIdentityTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotEventCatalogTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotComparisonServiceTests.cs`
+- Test: `src/WinRestoreKit.Tests/RestoreSetViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/ConfirmViewModelTests.cs`
+- Test: `src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs`
+- Test: `src/WinRestoreKit.Tests/BackupManifestTests.cs`
+- Test: `src/WinRestoreKit.Tests/RestorePlanTests.cs`
+- Test: `src/WinRestoreKit.Tests/SnapshotGateConsentTests.cs`
+- Test: `src/WinRestoreKit.Tests/ExplorerRestartPromptTests.cs`
+
+**Interfaces:**
+- Consumes: all final Application/Core/WPF contracts and Task 2/Task 4 evidence.
+- Produces: a signed-off acceptance record with evidence that the WPF app is the sole shipping app, no Core safety rule regressed, no legacy path remains, and the release executable is runnable.
+
+- [ ] **Step 1: Run the final focused safety and migration regression suite**
+
+ Run:
+
+ ```powershell
+ dotnet build src\WinRestoreKit.sln -c Release
+ dotnet test src\WinRestoreKit.sln -c Release --no-build --filter "FullyQualifiedName~ShippingIdentityTests|FullyQualifiedName~SnapshotEventCatalogTests|FullyQualifiedName~SnapshotComparisonServiceTests|FullyQualifiedName~RestoreSetViewModelTests|FullyQualifiedName~ConfirmViewModelTests|FullyQualifiedName~BackupDestinationLifecycleTests|FullyQualifiedName~BackupManifestTests|FullyQualifiedName~RestorePlanTests|FullyQualifiedName~SnapshotGateConsentTests|FullyQualifiedName~ExplorerRestartPromptTests"
+ dotnet test src\WinRestoreKit.sln -c Release --no-build
+ ```
+
+ Expected: both selected and full suites report `Failed: 0`. Treat any failure as a regression until it is reproduced, fixed in its owning layer, and covered by the test that failed.
+
+- [ ] **Step 2: Review the final diff for prohibited layering and safety regressions**
+
+ Review the completed diff against these concrete questions:
+
+ | Review question | Required answer |
+ | --- | --- |
+ | Does any `WinRestoreKit.Application` source reference WPF or WinForms? | No. Application remains framework-neutral. |
+ | Does any WPF view/view model parse a `.reg`, manifest, or payload body? | No. It renders Application/Core models only. |
+ | Can Compare write to a snapshot or live system? | No. It uses read scopes and `HasDriftedFrom` evidence only. |
+ | Can Failed/Unreadable Timeline events restore or influence retention? | No. They remain diagnostic-only and session failures do not retain cleanup data. |
+ | Can a WPF restore bypass `RestorePlan`, `SnapshotGate`, `RestoreScope`, `RestoreDispatch`, incomplete-snapshot consent, or `ExplorerRestartPrompt`? | No. Confirm delegates to the existing Application orchestration path. |
+ | Does a user see an invented reboot requirement? | No. Only existing process, Explorer restart, and sign-out impacts render. |
+ | Is there an ownerless dialog or a WinForms owner seam? | No. WPF dialog services own modal windows. |
+ | Does any app identity/version path diverge? | No. Linked raw source, compiled WPF assembly, release tag, and GitHub Release use one normalized three-part version. |
+
+ Run the repository checks from Task 3 Step 8 again. In addition, verify Application does not import either UI framework:
+
+ ```powershell
+ $applicationUi = git grep -nE 'System\.Windows\.Forms|System\.Windows\.(Controls|Window|Application)' -- 'src/WinRestoreKit.Application/**/*.cs' 'src/WinRestoreKit.Application/**/*.csproj'
+ if ($applicationUi) { throw "Application layer references a UI framework:`n$applicationUi" }
+ ```
+
+ Expected: no output from the check.
+
+- [ ] **Step 3: Request the repository's Windows safety review for the final WPF restore/dialog diff**
+
+ Give `.claude/agents/windows-safety-reviewer.md` the final diff touching `src/WinRestoreKit.Application/`, `src/WinRestoreKit.Wpf/Services/WpfRunUi.cs`, WPF dialog views, restore view models, and release settings. Require evidence-backed findings only.
+
+ Expected: no unresolved finding that permits a registry import, process closure, profile overwrite, restore prompt bypass, ownerless dialog, silent failure, or unintended elevated browser launch. Record reviewer identity, reviewed commit, and disposition in `docs/superpowers/verification/2026-08-09-wpf-release.md`.
+
+- [ ] **Step 4: Record final acceptance evidence and commit it**
+
+ Add this checklist to both verification records and mark an item only with the command/output or desktop observation that proved it:
+
+ ```markdown
+ - [ ] WPF Timeline + Compare is the shipping home and restore workflow.
+ - [ ] All current workflows have a verified WPF equivalent.
+ - [ ] Existing snapshot gates and restore safety behavior remain enforced.
+ - [ ] Failed attempts are visible and non-restorable.
+ - [ ] Keyboard, UIA, reduced motion, themes, DPI, and narrow layout passed real-desktop verification.
+ - [ ] All required screenshot baselines were reviewed.
+ - [ ] Core, Application, WPF, and full solution tests passed.
+ - [ ] No obsolete WinForms project, source, resource, control, helper, test, or compatibility path remains.
+ - [ ] Publish contains exactly one self-contained `WinRestoreKit.exe`.
+ - [ ] The executable passed the clean Windows desktop smoke and update-version coherence checks.
+ ```
+
+ Commit the evidence-only final review:
+
+ ```powershell
+ git add docs\superpowers\verification\2026-08-09-wpf-cutover.md docs\superpowers\verification\2026-08-09-wpf-release.md
+ git commit -m "docs: complete WPF cutover regression review"
+ ```
+
+## Completion Criteria
+
+The migration is complete only when the real-desktop gate preceded WinForms deletion, the WPF project is the sole shipping `WinRestoreKit` app, `src/WinRestoreKit/Properties/AssemblyInfo.cs` remains the linked raw version source, all relevant Core/Application/WPF tests are green, the code tree contains no WinForms shell or compatibility artifacts, and the final publish directory contains exactly one verified self-contained `WinRestoreKit.exe` that ran successfully on a real Windows desktop.
diff --git a/docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md b/docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md
new file mode 100644
index 0000000..c051901
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-09-wpf-foundation-application-shell.md
@@ -0,0 +1,1454 @@
+# WPF Foundation Application Shell Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Introduce a framework-neutral shared application layer and a runnable side-by-side WPF shell whose only workspace is an honest, empty Timeline state, while preserving the runnable WinForms application and all Core backup/restore semantics.
+
+**Architecture:** `WinRestoreKit.Application` is a `net8.0-windows` class library because it consumes the Windows-targeted Core project, but it has neither `UseWindowsForms` nor `UseWPF` and contains no framework types. It owns shared orchestration, neutral result/UI contracts, registry-backed application settings, and update/version logic; framework projects use friend access instead of widening those migration-only types. `WinRestoreKit.Wpf` is an MVVM host that composes WPF dispatcher, dialog, log, theme, settings, About, and update adapters, then renders a real empty Timeline view without manufacturing snapshot data.
+
+**Tech Stack:** .NET 8 (`net8.0-windows`), C# 12-compatible SDK project settings, WPF/XAML, existing WinRestoreKit.Core, xUnit 2.9.3, Microsoft.Win32 registry APIs, `HttpClient`, `Microsoft.Win32.SystemEvents`.
+
+## Global Constraints
+
+- Preserve `WinRestoreKit.Core` backup/restore semantics, existing snapshot format, `RestorePlan`, `SnapshotGate`, `RestoreScope`, `RestoreDispatch`, `ExplorerRestartPrompt`, and every existing Core result/safety decision.
+- `WinRestoreKit.Application` targets `net8.0-windows` solely because Core does; it MUST contain no `System.Windows.Forms`, `System.Windows`, WPF XAML, WinForms/WPF project property, or UI-framework reference.
+- Keep namespace `WinRestoreKit` for all Application contracts moved from the app; Application exposes internals to the still-runnable `WinRestoreKit` WinForms assembly, `WinRestoreKit.Wpf`, and `WinRestoreKit.Tests` with its own assembly-level `InternalsVisibleTo` attributes.
+- Keep the WinForms project runnable throughout this plan. It continues to reference Core directly for its existing internal Core consumers and additionally references Application for moved app-layer types.
+- Replace `RunSummary.Icon : MessageBoxIcon` with `RunSummary.Severity : RunSeverity`; `RunSeverity` values are exactly `Information`, `Warning`, and `Error`.
+- Replace the WinForms-typed `IRunUi.Owner` with framework-neutral `object DialogOwner { get; }`. No WinForms or WPF type crosses Application; each shell supplies its current native dialog owner as an opaque object.
+- Preserve Core `AppStoreApps.RestoreDialog : Action` and the exact existing AppStore restore outcome semantics. The moved orchestrator calls `await appStoreApps.RestoreAsync(currentRestorePath, ui.DialogOwner)`; do not introduce a new restore-dialog callback or fallback result path.
+- The WPF shell has no permanent rail, dashboard cards, fake timeline entries, comparison data, restore selection, backup-selection view, progress view, or cutover deletion in this plan.
+- Use exactly the new user-facing theme labels **Follow system**, **Light**, and **Dark**. Store the WPF preference as a DWORD `ThemeMode` under `HKCU\Software\WinRestoreKit`; do not reuse the WinForms-only `PaletteMode` setting.
+- WPF visual resources use neutral Windows surfaces, mineral-blue actions, restrained coral warnings, Segoe UI Variable interface text, and the linked packaged `IBMPlexMono-Regular.ttf` only for technical/log styles. State is conveyed by text/icon as well as color.
+- The side-by-side WPF project has assembly/executable identity `WinRestoreKit.Wpf`. It links the existing `app.manifest` and `WinRestoreKit.ico`, preserving `highestAvailable` and `longPathAware`, but does **not** compile the existing `Properties/AssemblyInfo.cs` and does **not** set `GenerateAssemblyInfo=false`.
+- Preserve the final publish contract in WPF project properties: self-contained `win-x64`, single-file, native-library self-extract, compression, and `PublishTrimmed=false`. The later cutover plan changes the WPF identity to `WinRestoreKit` and links the existing AssemblyInfo source at its exact physical path.
+- `src/WinRestoreKit/Properties/AssemblyInfo.cs` remains at that exact path and retains the exact three-part `[assembly: AssemblyFileVersion("x.y.z")]` source consumed by the GitHub raw fallback. Do not set competing version properties or attributes in any project.
+- Tests remain in `src/WinRestoreKit.Tests`. Preserve pure existing tests; retain WinForms construction tests while WinForms exists; add an STA helper before constructing WPF runtime objects.
+- Every code change below follows a red → green cycle. Run only the named focused test/build command at each step; do not run formatters, linters, or broad test suites as part of this foundation plan.
+
+---
+
+## File Structure and Ownership
+
+| Path | Responsibility |
+| --- | --- |
+| `src/WinRestoreKit.Application/WinRestoreKit.Application.csproj` | Framework-neutral shared application library; references Core and grants friends access to WPF/tests. |
+| `src/WinRestoreKit.Application/Properties/AssemblyInfo.cs` | Application-only friend declarations for WPF and tests. |
+| `src/WinRestoreKit.Application/Orchestration/{IRunUi,RunCoordinator,RunControl,BackupRestoreOrchestrator}.cs` | Moved orchestration and neutral interaction surface. `BackupRestoreOrchestrator.cs` also retains `ProgressMetricValues` and `ProgressMetrics`. |
+| `src/WinRestoreKit.Application/Results/RunSummary.cs` | Moved run grammar/state/summary and neutral severity. |
+| `src/WinRestoreKit.Application/Settings/{BackupRootRegistry,ThemeMode,IThemeSettings,RegistryThemeSettings}.cs` | Registry-backed custom-root and theme-preference contracts, without UI framework types. |
+| `src/WinRestoreKit.Application/Updates/{VersionInfo,UpdateVerdict,UpdateCheckResult,IUpdateCheckService,UpdateCheckService}.cs` | Version normalization and GitHub-release/raw-AssemblyInfo update decision/fetch service without dialog code. |
+| `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj` | Side-by-side WPF app configuration, linked manifest/icon/font, final publish properties, Application/Core references. |
+| `src/WinRestoreKit.Wpf/{App.xaml,App.xaml.cs,MainWindow.xaml,MainWindow.xaml.cs}` | WPF resources, startup composition, and top-level content host. |
+| `src/WinRestoreKit.Wpf/Infrastructure/{ObservableObject,DelegateCommand}.cs` | Minimal dependency-free MVVM notifications and commands. |
+| `src/WinRestoreKit.Wpf/ViewModels/{ShellViewModel,TimelineWorkspaceViewModel,SettingsViewModel,AboutViewModel}.cs` | Shell navigation and state; the Timeline VM models only the actual empty state. |
+| `src/WinRestoreKit.Wpf/Views/{TimelineWorkspaceView,SettingsView,AboutView}.xaml` and code-behind | Declarative, framework-owned presentation with no registry/payload parsing. |
+| `src/WinRestoreKit.Wpf/Themes/{Controls,Light,Dark}.xaml` | Dynamic resource keys and the approved Light/Dark token dictionaries. |
+| `src/WinRestoreKit.Wpf/Services/{ISystemThemeDetector,WindowsThemeDetector,IThemeService,WpfThemeService,IWpfDialogService,WpfDialogService,IExternalLinkService,ExternalLinkService,WpfUpdatePresenter,IRunPresentation,IRunDialogService,WpfDispatcher,WpfRunUi,WpfLogSink}.cs` | Shell-owned dispatcher/dialog/log/theme/update adapters. The run interfaces are ready for later workflow views but no workflow is rendered in this plan. |
+| `src/WinRestoreKit/Orchestration/*`, `src/WinRestoreKit/Results/RunSummary.cs`, `src/WinRestoreKit/Helpers/{BackupRootRegistry,UpdateCheck}.cs` | Deleted after their contents move to Application; no forwarding copies remain. |
+| `src/WinRestoreKit/{WinRestoreKit.csproj,Program.cs,Views/ProgressPageView.cs,Views/AboutPageView.cs,Helpers/WinFormsUpdatePresenter.cs}` | Existing shell rebuilt against Application, preserving modal WinForms ownership and startup behavior. |
+| `src/WinRestoreKit.Core/WinRestoreKit.Core.csproj` | Adds Application and WPF as internal friends without changing Core type access levels. |
+| `src/WinRestoreKit.Tests/{WinRestoreKit.Tests.csproj,AssemblyInfo.cs,WpfTestHost.cs,ApplicationBoundaryTests.cs,RunSummaryTests.cs,RunCoordinatorTests.cs,RunControlTests.cs,ThemeSettingsTests.cs,ThemeServiceTests.cs,VersionParsingTests.cs,UpdateCheckVerdictTests.cs,WpfShellTests.cs}` | Existing tests migrated to moved symbols plus focused new Application/WPF tests and STA construction helper. |
+| `src/WinRestoreKit.sln` | Adds Application and WPF projects without removing WinForms, Core, or Tests. |
+
+## Definitive Interfaces Produced by This Plan
+
+These signatures are the integration boundary for the Timeline, Compare/Confirm, Backup/Progress, and Cutover plans.
+
+```csharp
+// src/WinRestoreKit.Application/Orchestration/IRunUi.cs
+namespace WinRestoreKit;
+
+internal interface IRunUi
+{
+ void SetProgressText(string text);
+ void SetProgressPercent(int percent);
+ void SetProgressDetail(string groupInfo, string elapsed, string remaining, string throughput,
+ long bytesWritten, int errors, int warnings);
+ void ShowSummary(RunSummary summary, string caption, IReadOnlyList outcomes);
+ IReadOnlyList ShowConsentDialog(RestorePlan plan);
+ object DialogOwner { get; }
+ bool ConfirmSnapshotOverride(string text, string caption);
+ void ShowPlanCompositionError(string text, string caption);
+ void SetExplorerRestartVisible(bool visible);
+}
+
+// src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs
+namespace WinRestoreKit;
+
+internal sealed class BackupRestoreOrchestrator
+{
+ internal BackupRestoreOrchestrator(IRunUi ui, RunControl runControl = null);
+ internal Task RunBackup(IReadOnlyList modules, string destination,
+ string snapshotName, SnapshotCompression compression);
+ internal Task RunRestore(IReadOnlyList modules, string backupPath);
+}
+
+// src/WinRestoreKit.Application/Results/RunSummary.cs
+namespace WinRestoreKit;
+
+internal enum RunSeverity { Information, Warning, Error }
+
+internal sealed class RunSummary
+{
+ public RunState State { get; private set; }
+ public string Headline { get; private set; }
+ public string Detail { get; private set; }
+ public RunSeverity Severity { get; }
+ internal static RunSummary For(IReadOnlyList outcomes, bool ran, RunVerb verb,
+ string because = null);
+ internal static RunSummary Incomplete(IReadOnlyList outcomes, RunVerb verb,
+ string detail);
+ internal static RunSummary Canceled(RunVerb verb);
+}
+
+// src/WinRestoreKit.Application/Settings/ThemeMode.cs
+namespace WinRestoreKit;
+
+internal enum ThemeMode { FollowSystem = 0, Light = 1, Dark = 2 }
+
+internal interface IThemeSettings
+{
+ ThemeMode ReadThemeMode();
+ void WriteThemeMode(ThemeMode mode);
+}
+
+// src/WinRestoreKit.Application/Updates/IUpdateCheckService.cs
+namespace WinRestoreKit;
+
+internal interface IUpdateCheckService
+{
+ Task CheckAsync(string currentVersion, CancellationToken cancellationToken);
+}
+```
+
+```csharp
+// src/WinRestoreKit.Wpf/Services/WpfRunUi.cs
+namespace WinRestoreKit.Wpf.Services;
+
+internal sealed class WpfRunUi : IRunUi
+{
+ internal WpfRunUi(Dispatcher dispatcher, IRunPresentation presentation,
+ IRunDialogService dialogs, Func ownerProvider);
+ object IRunUi.DialogOwner { get; }
+}
+
+// src/WinRestoreKit.Wpf/Services/IRunPresentation.cs
+internal interface IRunPresentation
+{
+ void SetProgressText(string text);
+ void SetProgressPercent(int percent);
+ void SetProgressDetail(string groupInfo, string elapsed, string remaining, string throughput,
+ long bytesWritten, int errors, int warnings);
+ void ShowSummary(RunSummary summary, string caption, IReadOnlyList outcomes);
+ void SetExplorerRestartVisible(bool visible);
+}
+
+// src/WinRestoreKit.Wpf/Services/IRunDialogService.cs
+internal interface IRunDialogService
+{
+ IReadOnlyList ShowRestoreConsent(RestorePlan plan);
+ bool ConfirmSnapshotOverride(string text, string caption);
+ void ShowPlanCompositionError(string text, string caption);
+}
+
+// src/WinRestoreKit.Wpf/ViewModels/ShellViewModel.cs
+internal sealed class ShellViewModel : ObservableObject
+{
+ internal ShellViewModel(IThemeService themes, WpfUpdatePresenter updates,
+ string currentVersion);
+ public object CurrentWorkspace { get; private set; }
+ public string WorkflowLabel { get; private set; }
+ public ICommand ShowTimelineCommand { get; }
+ public ICommand ShowSettingsCommand { get; }
+ public ICommand ShowAboutCommand { get; }
+ internal void ShowTimeline();
+ internal void NavigateTo(object workspace, string workflowLabel);
+}
+```
+
+`WpfRunUi` is not composed by the empty-shell startup; later real Backup/Progress and Compare/Confirm workspaces provide concrete `IRunPresentation` and `IRunDialogService` implementations. This is an adapter boundary, not a visible unfinished workflow.
+
+### Task 1: Add the application/WPF build topology and test access boundaries
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/WinRestoreKit.Application.csproj`
+- Create: `src/WinRestoreKit.Application/Properties/AssemblyInfo.cs`
+- Create: `src/WinRestoreKit.Wpf/WinRestoreKit.Wpf.csproj`
+- Create: `src/WinRestoreKit.Wpf/App.xaml`
+- Create: `src/WinRestoreKit.Wpf/App.xaml.cs`
+- Create: `src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs`
+- Create: `src/WinRestoreKit.Tests/ApplicationBoundaryTests.cs`
+- Modify: `src/WinRestoreKit.Core/WinRestoreKit.Core.csproj:36-46`
+- Modify: `src/WinRestoreKit/WinRestoreKit.csproj:38-49`
+- Modify: `src/WinRestoreKit.Tests/WinRestoreKit.Tests.csproj:3-41`
+- Modify: `src/WinRestoreKit.sln`
+
+**Interfaces:**
+- Consumes: Existing `WinRestoreKit.Core` net8.0-windows target, its `WinRestoreKit`/`WinRestoreKit.Tests` internal friends, the existing manifest/icon, and the existing hand-maintained AssemblyInfo source.
+- Produces: Application and WPF projects in the solution; Core grants internals to `WinRestoreKit.Application` and `WinRestoreKit.Wpf`; Application grants internals to WinForms, WPF, and tests; Tests can reference Application/WPF and will receive the STA helper in Task 3.
+
+- [ ] **Step 1: Add empty project files and solution entries before adding behavior.**
+
+Create the two project files and use the SDK to add both to the existing solution. The Application project must target Windows only to match Core; it must not set either UI framework property. The WPF app has a temporary distinct assembly identity, links the existing manifest/icon/font, and includes the release properties without taking ownership of the version source.
+
+```xml
+
+
+
+ net8.0-windows
+ WinRestoreKit
+ WinRestoreKit.Application
+ disable
+ disable
+ AnyCPU
+ true
+
+
+
+
+
+
+
+
+
+ WinExe
+ net8.0-windows
+ true
+ WinRestoreKit.Wpf
+ WinRestoreKit.Wpf
+ ..\WinRestoreKit\app.manifest
+ ..\WinRestoreKit\WinRestoreKit.ico
+ win-x64
+ true
+ true
+ true
+ true
+ false
+ disable
+ disable
+ AnyCPU
+ true
+
+
+
+
+
+
+
+```
+
+Create the minimal WPF application definition now so the side-by-side project has a generated STA entry point and can build before Task 6 adds composition:
+
+```xml
+
+
+```
+
+```csharp
+// src/WinRestoreKit.Wpf/App.xaml.cs
+using System.Windows;
+
+namespace WinRestoreKit.Wpf;
+
+public partial class App : Application
+{
+}
+```
+
+Run:
+
+```powershell
+dotnet sln src\WinRestoreKit.sln add src\WinRestoreKit.Application\WinRestoreKit.Application.csproj src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj
+```
+
+Expected: both projects appear in `src\WinRestoreKit.sln`; neither project removes or retargets WinForms, Core, or Tests.
+
+- [ ] **Step 2: Write the failing project-boundary test.**
+
+Add a direct use of the Application `RunControl` type. Do not create that type until Task 2; this establishes that the test project references the new assembly before the moved code exists.
+
+```csharp
+// src/WinRestoreKit.Tests/ApplicationBoundaryTests.cs
+extern alias Application;
+
+using System.Linq;
+using ApplicationWinRestoreKit = Application::WinRestoreKit;
+using Xunit;
+
+namespace WinRestoreKit.Tests
+{
+ public class ApplicationBoundaryTests
+ {
+ [Fact]
+ public void ApplicationRunControl_IsAvailableWithoutConstructingAUiFrameworkObject()
+ {
+ using (ApplicationWinRestoreKit.RunControl control =
+ new ApplicationWinRestoreKit.RunControl())
+ {
+ Assert.False(control.IsPaused);
+ Assert.False(control.IsCancellationRequested);
+ }
+ }
+ }
+}
+```
+
+Extend the test project now so this fails for the missing moved type rather than silently exercising the old app assembly:
+
+```xml
+
+
+ true
+ true
+
+
+
+ global;Application
+
+
+
+```
+
+Run:
+
+```powershell
+dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj --filter FullyQualifiedName~ApplicationBoundaryTests
+```
+
+Expected: FAIL at compile time because `RunControl` has not yet been created in `WinRestoreKit.Application`.
+
+- [ ] **Step 3: Establish all internal-friend and project-reference boundaries.**
+
+Add the Application and WPF friends to Core without changing Core type modifiers, add WinForms/WPF/tests as Application friends, and add Application as an additional reference from the still-runnable WinForms project. The test project retains its existing WinForms project reference because existing tests still construct the old shell during migration.
+
+```xml
+
+
+
+
+
+
+```
+
+```csharp
+// src/WinRestoreKit.Application/Properties/AssemblyInfo.cs
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("WinRestoreKit")]
+[assembly: InternalsVisibleTo("WinRestoreKit.Wpf")]
+[assembly: InternalsVisibleTo("WinRestoreKit.Tests")]
+
+// src/WinRestoreKit.Wpf/Properties/AssemblyInfo.cs
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("WinRestoreKit.Tests")]
+```
+
+Do not add `GenerateAssemblyInfo=false` to either new project. Do not copy or link `src/WinRestoreKit/Properties/AssemblyInfo.cs` into WPF.
+
+- [ ] **Step 4: Run focused project topology verification.**
+
+Run:
+
+```powershell
+dotnet build src\WinRestoreKit.Application\WinRestoreKit.Application.csproj
+dotnet build src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj
+dotnet build src\WinRestoreKit\WinRestoreKit.csproj
+```
+
+Expected: the first command succeeds with an Application DLL that has no UI-framework project dependency; the second succeeds as `WinRestoreKit.Wpf`; the third succeeds as the existing `WinRestoreKit` WinForms executable. The focused test still fails only because Task 2 has not moved `RunControl`.
+
+- [ ] **Step 5: Commit the topology only.**
+
+```powershell
+git add src\WinRestoreKit.sln src\WinRestoreKit.Core\WinRestoreKit.Core.csproj src\WinRestoreKit\WinRestoreKit.csproj src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj src\WinRestoreKit.Tests\ApplicationBoundaryTests.cs src\WinRestoreKit.Application\WinRestoreKit.Application.csproj src\WinRestoreKit.Application\Properties\AssemblyInfo.cs src\WinRestoreKit.Wpf\WinRestoreKit.Wpf.csproj src\WinRestoreKit.Wpf\App.xaml src\WinRestoreKit.Wpf\App.xaml.cs src\WinRestoreKit.Wpf\Properties\AssemblyInfo.cs
+git commit -m "build: add application and WPF migration projects"
+```
+
+### Task 2: Extract shared run state/orchestration and neutralize its UI contract
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Orchestration/IRunUi.cs`
+- Create: `src/WinRestoreKit.Application/Orchestration/RunCoordinator.cs`
+- Create: `src/WinRestoreKit.Application/Orchestration/RunControl.cs`
+- Create: `src/WinRestoreKit.Application/Orchestration/BackupRestoreOrchestrator.cs`
+- Create: `src/WinRestoreKit.Application/Results/RunSummary.cs`
+- Create: `src/WinRestoreKit.Application/Settings/BackupRootRegistry.cs`
+- Verify unchanged: `src/WinRestoreKit/MainForm.cs:17-303` — `StartBackup`, `StartRestore`, and `OnRunningChanged` continue to resolve the moved `RunCoordinator` by the same `WinRestoreKit` namespace; do not redesign its rail/navigation in this foundation plan.
+- Modify: `src/WinRestoreKit/Views/ProgressPageView.cs:15-25,399-445,685-790`
+- Modify: `src/WinRestoreKit/Orchestration/BackupRestoreOrchestrator.cs:1-1325` then delete it
+- Modify: `src/WinRestoreKit/Orchestration/IRunUi.cs:1-52` then delete it
+- Modify: `src/WinRestoreKit/Orchestration/RunCoordinator.cs:1-47` then delete it
+- Modify: `src/WinRestoreKit/Orchestration/RunControl.cs:1-106` then delete it
+- Modify: `src/WinRestoreKit/Results/RunSummary.cs:1-169` then delete it
+- Modify: `src/WinRestoreKit/Helpers/BackupRootRegistry.cs:1-104` then delete it
+- Modify: `src/WinRestoreKit.Tests/RunSummaryTests.cs`
+- Modify: `src/WinRestoreKit.Tests/ApplicationBoundaryTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RunCoordinatorTests.cs`
+- Modify: `src/WinRestoreKit.Tests/RunControlTests.cs`
+- Modify: `src/WinRestoreKit.Tests/BackupDestinationLifecycleTests.cs`
+- Verify unchanged: `src/WinRestoreKit.Tests/RestoreDialogOwnerTests.cs:1-64` — its existing `AppStoreApps.RestoreDialog` hook must continue to prove that the opaque owner passed to the Core overload reaches the registered dialog and returns the current `Skipped` outcome.
+
+**Interfaces:**
+- Consumes: Task 1 project/friend topology; Core `BackupBase`, `ModuleOutcome`, `RestorePlan`, `LogHelper`, `SnapshotGate`, `RestoreScope`, `RestoreDispatch`, `ExplorerRestartPrompt`, and `Conf.AppStoreApps`.
+- Produces: The exact Application `IRunUi`, `RunControl`, `RunCoordinator`, `BackupRestoreOrchestrator`, `RunSummary`, `RunSeverity`, and `BackupRootRegistry` contracts declared above. WinForms remains an `IRunUi` implementation and supplies its dialog owner only as the opaque `object DialogOwner`.
+
+- [ ] **Step 1: Write failing severity and neutral-owner-contract tests.**
+
+Replace the obsolete WinForms icon assertions in `RunSummaryTests` with neutral severity assertions, and add a contract check that pins `IRunUi.DialogOwner` to `object` while proving the WinForms-typed `Owner` property has gone. Do not modify `RestoreDialogOwnerTests`; it is the existing focused regression test for the preserved Core `Action` dialog seam and is re-run with this task’s focused command.
+
+```csharp
+[Fact]
+public void DidNotRun_IsWarningWithoutAWinFormsMessageBoxIcon()
+{
+ RunSummary summary = RunSummary.For(new List(), false, RunVerb.Backup,
+ "the destination is empty");
+
+ Assert.Equal(RunSeverity.Warning, summary.Severity);
+ var dialogOwner = typeof(IRunUi).GetProperty("DialogOwner");
+ Assert.NotNull(dialogOwner);
+ Assert.Equal(typeof(object), dialogOwner.PropertyType);
+ Assert.Null(typeof(IRunUi).GetProperty("Owner"));
+}
+
+[Fact]
+public void IncompleteRun_IsWarning()
+{
+ RunSummary summary = RunSummary.Incomplete(new List(), RunVerb.Restore,
+ "The pre-restore snapshot was incomplete.");
+
+ Assert.Equal(RunSeverity.Warning, summary.Severity);
+}
+
+```
+
+Add this separate test to `ApplicationBoundaryTests.cs`, which already imports `ApplicationWinRestoreKit` through the direct Application assembly alias:
+
+```csharp
+[Fact]
+public void ApplicationAssembly_HasNoWinFormsOrWpfAssemblyReference()
+{
+ string[] references = typeof(ApplicationWinRestoreKit.RunControl).Assembly.GetReferencedAssemblies()
+ .Select(reference => reference.Name)
+ .ToArray();
+
+ Assert.DoesNotContain("System.Windows.Forms", references);
+ Assert.DoesNotContain("PresentationFramework", references);
+ Assert.DoesNotContain("WindowsBase", references);
+}
+```
+
+Run:
+
+```powershell
+dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~RunSummaryTests|FullyQualifiedName~ApplicationBoundaryTests"
+```
+
+Expected: FAIL because the Application project does not yet contain `RunSummary`, `IRunUi`, or `RunControl`.
+
+- [ ] **Step 2: Move run types verbatim first, then make the two intentional contract changes.**
+
+Move the existing bodies from the old `Orchestration`, `Results`, and `BackupRootRegistry` paths to the exact Application paths. Preserve all existing namespace, type names, method bodies, cancellation behavior, manifest writes, `SnapshotGate` sequencing, cleanup/retention rules, and `ProgressMetrics` formulas. Delete the old source files after the move; do not retain forwarding files.
+
+Replace the WinForms-only `RunSummary.Icon` property with this mapping:
+
+```csharp
+internal enum RunSeverity
+{
+ Information,
+ Warning,
+ Error
+}
+
+public RunSeverity Severity
+ => State == RunState.Problems || State == RunState.DidNotRun
+ ? RunSeverity.Warning
+ : RunSeverity.Information;
+```
+
+Replace the former WinForms-typed `Owner` property in the moved interface with this exact neutral abstraction:
+
+```csharp
+///
+/// Opaque owner for a shell-native modal dialog. The Application layer never casts it.
+///
+object DialogOwner { get; }
+```
+
+At the existing AppStore branch in `BackupRestoreOrchestrator.RunRestore`, preserve the Core seam and all current result semantics; change only the property name and type at the Application boundary:
+
+```csharp
+ModuleResult outcome = config is Conf.AppStoreApps appStoreApps
+ ? await appStoreApps.RestoreAsync(currentRestorePath, ui.DialogOwner)
+ : await config.RestoreAsync(currentRestorePath);
+```
+
+Do not modify `Conf.AppStoreApps`, its `RestoreDialog : Action` seam, Core artifact handling, restore order, or the AppStore module’s existing `Skipped`/failure outcome semantics in this task.
+
+- [ ] **Step 3: Adapt the still-shipping WinForms run view to the neutral contract.**
+
+Replace the explicit `IRunUi.Owner` implementation in `ProgressPageView` with this exact opaque owner property. It returns the same current Form-or-control object currently passed to `AppStoreApps.RestoreAsync`; do not construct `RestAppsForm` here or change its dialog lifecycle.
+
+```csharp
+object IRunUi.DialogOwner => (object)FindForm() ?? this;
+```
+
+Keep every existing `ShowConsentDialog`, snapshot-override confirmation, plan-composition error, Explorer restart, progress, summary, log-sink, cancellation, and dispatcher behavior. Preserve the current title accent behavior by using the neutral severity:
+
+```csharp
+titleLabel.ForeColor = summary.Severity == RunSeverity.Warning
+ ? Theme.Current.Accent2_600
+ : Theme.Current.Text;
+```
+
+- [ ] **Step 4: Run the extracted pure-contract tests to verify they pass.**
+
+Run:
+
+```powershell
+dotnet test src\WinRestoreKit.Tests\WinRestoreKit.Tests.csproj --filter "FullyQualifiedName~ApplicationBoundaryTests|FullyQualifiedName~RunSummaryTests|FullyQualifiedName~RunCoordinatorTests|FullyQualifiedName~RunControlTests|FullyQualifiedName~BackupDestinationLifecycleTests|FullyQualifiedName~RestoreDialogOwnerTests"
+```
+
+Expected: PASS. The existing coordinator test still admits exactly one concurrent run; control tests still release paused waiters on cancellation; backup-root lifecycle behavior still records valid custom roots; new summary tests prove severity plus an `object`-typed `IRunUi.DialogOwner` without `IRunUi.Owner`; `RestoreDialogOwnerTests` proves the unchanged Core AppStore dialog seam receives the supplied opaque owner.
+
+- [ ] **Step 5: Verify the Application boundary and WinForms compatibility compile independently.**
+
+Run:
+
+```powershell
+dotnet build src\WinRestoreKit.Application\WinRestoreKit.Application.csproj
+dotnet build src\WinRestoreKit\WinRestoreKit.csproj
+```
+
+Expected: both PASS. Application compiles against Core with no WinForms/WPF reference; WinForms compiles its existing `ProgressPageView` against the moved Application types.
+
+- [ ] **Step 6: Commit the complete orchestration extraction.**
+
+```powershell
+git add -A src\WinRestoreKit.Application src\WinRestoreKit\Orchestration src\WinRestoreKit\Results src\WinRestoreKit\Helpers\BackupRootRegistry.cs src\WinRestoreKit\Views\ProgressPageView.cs src\WinRestoreKit.Tests\ApplicationBoundaryTests.cs src\WinRestoreKit.Tests\RunSummaryTests.cs src\WinRestoreKit.Tests\RunCoordinatorTests.cs src\WinRestoreKit.Tests\RunControlTests.cs src\WinRestoreKit.Tests\BackupDestinationLifecycleTests.cs
+git commit -m "refactor: move shared run orchestration to application"
+```
+
+### Task 3: Add registry-backed WPF theme settings and dynamic Light/Dark/Follow-system resources
+
+**Files:**
+- Create: `src/WinRestoreKit.Application/Settings/ThemeMode.cs`
+- Create: `src/WinRestoreKit.Application/Settings/IThemeSettings.cs`
+- Create: `src/WinRestoreKit.Application/Settings/RegistryThemeSettings.cs`
+- Create: `src/WinRestoreKit.Wpf/Themes/Controls.xaml`
+- Create: `src/WinRestoreKit.Wpf/Themes/Light.xaml`
+- Create: `src/WinRestoreKit.Wpf/Themes/Dark.xaml`
+- Create: `src/WinRestoreKit.Wpf/Services/ISystemThemeDetector.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/WindowsThemeDetector.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/IThemeService.cs`
+- Create: `src/WinRestoreKit.Wpf/Services/WpfThemeService.cs`
+- Create: `src/WinRestoreKit.Tests/WpfTestHost.cs`
+- Create: `src/WinRestoreKit.Tests/ThemeSettingsTests.cs`
+- Create: `src/WinRestoreKit.Tests/ThemeServiceTests.cs`
+
+**Interfaces:**
+- Consumes: Application’s no-UI project boundary from Task 1; `Microsoft.Win32.Registry`; WPF resource dictionaries; and `SystemEvents.UserPreferenceChanged`.
+- Produces: `ThemeMode`, `IThemeSettings`, `RegistryThemeSettings`, `ISystemThemeDetector`, `IThemeService`, and `WpfThemeService`. The WPF shell reads only these neutral settings; it does not reuse WinForms `Theme`, `PaletteMode`, `Voltage`, or `Flux`.
+
+- [ ] **Step 1: Write the failing settings and effective-theme tests.**
+
+Use a unique HKCU subkey in the persistence test, delete it in `finally`, and use fakes for the system detector so the visual decision is deterministic.
+
+Create the STA helper as test infrastructure before writing the WPF-specific test. It creates no `Application` object and never displays a window:
+
+```csharp
+// src/WinRestoreKit.Tests/WpfTestHost.cs
+using System;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Windows.Threading;
+
+namespace WinRestoreKit.Tests
+{
+ internal static class WpfTestHost
+ {
+ internal static void Run(Action action)
+ => Run(() =>
+ {
+ action();
+ return null;
+ });
+
+ internal static T Run