|
| 1 | +# WinSwift v3.4.0 Plan — Recoverability and Proof |
| 2 | + |
| 3 | +## 1. Context |
| 4 | + |
| 5 | +The last three releases each added one half of a safety story and stopped: |
| 6 | + |
| 7 | +| Release | Added | Left open | |
| 8 | +|---|---|---| |
| 9 | +| v3.2.0 | Registry backup and system restore enforcement before bulk removal | Nothing consumes the backup automatically | |
| 10 | +| v3.3.0 | Desired-state verification with registry read-back and Appx checks | 10 of 112 features cannot be verified at all | |
| 11 | +| v3.3.0 | Static validation and Pester unit coverage | `Tests/Integration/` is empty; nothing exercises a real apply | |
| 12 | + |
| 13 | +WinSwift can now take a backup and can prove whether a change landed. It still cannot |
| 14 | +*undo a failed run on its own*, and the proof does not cover the features most likely to |
| 15 | +fail. v3.4.0 closes that loop. |
| 16 | + |
| 17 | +Everything in this release extends infrastructure that already ships. No new subsystem is |
| 18 | +introduced. |
| 19 | + |
| 20 | +## 2. Scope |
| 21 | + |
| 22 | +**Headline:** automatic rollback when an apply run fails. |
| 23 | + |
| 24 | +**Supporting:** |
| 25 | + |
| 26 | +- Verification coverage from 102/112 to 112/112. |
| 27 | +- An integration test suite that exercises apply, failure, and rollback for real. |
| 28 | +- Field validation of the 24H2 DISM fallback repaired in #9. |
| 29 | + |
| 30 | +## 3. Track 1 — Automatic rollback on failed apply |
| 31 | + |
| 32 | +### 3.1 Why this is mostly wiring |
| 33 | + |
| 34 | +`Invoke-AllChanges` in `Scripts/Features/InvokeChanges.ps1` already runs in phases: |
| 35 | + |
| 36 | +``` |
| 37 | +Phase 1 Registry backup <- rollback material is created here |
| 38 | +Phase 2 System restore point |
| 39 | +Phase 3 Apply features <- failures are counted here |
| 40 | +Phase 4 Undo features |
| 41 | +Final Report failures, Export-RunSummary |
| 42 | +``` |
| 43 | + |
| 44 | +The backup is taken *before* the apply phase, so at the moment of failure the material |
| 45 | +needed to recover already exists on disk. `Restore-RegistryBackupState -Backup $backup` in |
| 46 | +`Scripts/Features/RestoreRegistryBackup.ps1` is a complete, working restore entry point, |
| 47 | +including the loaded-hive path for `DefaultUserProfile` and `User:*` targets. |
| 48 | + |
| 49 | +`InvokeChanges.ps1` contains **zero** references to any restore function. The three missing |
| 50 | +pieces are a handle, a policy, and a call. |
| 51 | + |
| 52 | +### 3.2 What to build |
| 53 | + |
| 54 | +**A handle.** Phase 1 discards the backup object once written. Capture it into a |
| 55 | +run-scoped variable (`$script:RunRegistryBackup`) holding the backup payload and its file |
| 56 | +path, so Phase 3 can reach it without re-reading from disk. |
| 57 | + |
| 58 | +**A failure policy.** This is the one real design decision in the release, and it must be |
| 59 | +settled before code is written. `$script:RegistryImportFailures` already counts registry |
| 60 | +import failures, and `$script:AppRemovalFailures` counts removals. Neither currently |
| 61 | +triggers anything except a yellow warning in the Final phase. |
| 62 | + |
| 63 | +Proposed policy, to be confirmed: |
| 64 | + |
| 65 | +| Condition | Action | |
| 66 | +|---|---| |
| 67 | +| Any registry import failure | Prompt to roll back; roll back automatically under `-Unattend` | |
| 68 | +| App removal failure only | Do **not** roll back — removals are not restored by a registry backup, so rolling back the registry would misrepresent what was recovered | |
| 69 | +| User cancellation (`$script:CancelRequested`) | Roll back changes already applied in this run | |
| 70 | +| Failure during rollback itself | Abort, report loudly, leave the backup file in place and name it | |
| 71 | + |
| 72 | +The app-removal exclusion matters. A registry restore cannot bring back an uninstalled |
| 73 | +Appx package, so treating a removal failure as a rollback trigger would produce a run that |
| 74 | +claims to have recovered while leaving apps gone. |
| 75 | + |
| 76 | +**A call.** Invoke the restore between Phase 3 and Phase 4, before any undo work, so a |
| 77 | +failed apply is not compounded by undo operations against a half-applied system. |
| 78 | + |
| 79 | +### 3.3 Surfacing |
| 80 | + |
| 81 | +- Extend `Export-RunSummary` (`Scripts/Features/ExportRunSummary.ps1`) with a rollback |
| 82 | + section: whether it triggered, what condition triggered it, which roots were restored, |
| 83 | + and whether the restore itself succeeded. |
| 84 | +- Add a `-NoAutoRollback` switch for operators who would rather inspect a broken state |
| 85 | + than have it reverted underneath them. |
| 86 | +- Return a distinct exit code for "apply failed and was rolled back" so unattended callers |
| 87 | + can tell it apart from a plain failure. v3.3.0 already established exit code `2` for |
| 88 | + verification noncompliance; this needs its own value rather than reusing `2`. |
| 89 | + |
| 90 | +### 3.4 Acceptance criteria |
| 91 | + |
| 92 | +- A run with an injected registry import failure restores the affected roots and reports |
| 93 | + it in the run summary. |
| 94 | +- A run with an app removal failure and no registry failure does **not** roll back. |
| 95 | +- `-WhatIf` never restores anything and says what it would have done, matching the |
| 96 | + existing `Restore-RegistryBackupState` WhatIf branch. |
| 97 | +- A failed restore is reported with the backup file path so recovery can be finished by |
| 98 | + hand. |
| 99 | +- `-SkipRegistryBackup` disables auto-rollback with an explicit warning at run start, |
| 100 | + rather than failing silently at the moment it is needed. |
| 101 | + |
| 102 | +### 3.5 Upstream note |
| 103 | + |
| 104 | +Upstream `Raphire/Win11Debloat` has an open issue (#612) and PR (#613) for automatic |
| 105 | +registry rollback. Review both before finalizing the policy table — not to port the code, |
| 106 | +since `InvokeChanges.ps1` is on the deferred-port list in `UPSTREAM.md` and has diverged, |
| 107 | +but to avoid a gratuitously different failure model. |
| 108 | + |
| 109 | +## 4. Track 2 — Verification coverage to 112/112 |
| 110 | + |
| 111 | +### 4.1 The gap |
| 112 | + |
| 113 | +Of 112 features in `Config/Features.json`, 93 verify through `RegistryKey` read-back and 9 |
| 114 | +through a `VerificationAdapter`. Ten have neither and are silently unverifiable: |
| 115 | + |
| 116 | +| FeatureId | Why registry read-back cannot cover it | |
| 117 | +|---|---| |
| 118 | +| `RemoveApps`, `Apps`, `RemoveGamingApps`, `RemoveHPApps` | Package state, not a registry value | |
| 119 | +| `ForceRemoveEdge` | Package state plus filesystem leftovers | |
| 120 | +| `ClearStart`, `ClearStartAllUsers`, `ReplaceStart`, `ReplaceStartAllUsers` | Start layout lives in a per-user layout file | |
| 121 | +| `CreateRestorePoint` | An event, not a persistent desired state | |
| 122 | + |
| 123 | +### 4.2 Approach |
| 124 | + |
| 125 | +There is already a precedent in the codebase. `Test-FeatureApplied` in |
| 126 | +`Scripts/Features/GetCurrentTweakState.ps1` handles `DisableWidgets` by querying |
| 127 | +`Get-AppxPackage` and treating package absence as the applied state. The four app-removal |
| 128 | +features and `ForceRemoveEdge` follow that pattern directly. |
| 129 | + |
| 130 | +- **App removal features:** new `AppxAbsence` adapter resolving the feature's app list via |
| 131 | + the existing `Get-WinSwiftFeatureAppIds`, then checking installed *and* provisioned |
| 132 | + state. Provisioned state matters — a package can be uninstalled per-user while still |
| 133 | + provisioned and due to return on the next servicing pass. |
| 134 | +- **`ForceRemoveEdge`:** extend the above with the leftover shortcut paths and autostart |
| 135 | + values that `Remove-EdgeAutostartValue` already knows about. |
| 136 | +- **Start layout features:** new `StartLayout` adapter comparing the on-disk layout file |
| 137 | + against the expected shape. |
| 138 | +- **`CreateRestorePoint`:** do not add an adapter. It is an action, not a desired state. |
| 139 | + Mark it explicitly exempt in `Features.json` and have the verification engine report it |
| 140 | + as `NotApplicable` rather than counting it as an unverifiable gap. Add a unit assertion |
| 141 | + that this is the *only* permitted exemption, so the count cannot silently regress. |
| 142 | + |
| 143 | +### 4.3 Acceptance criteria |
| 144 | + |
| 145 | +- `Test-FeaturesJson.ps1` asserts every feature has `RegistryKey`, `VerificationAdapter`, |
| 146 | + or the explicit exemption — so a new feature cannot be added without a verification story. |
| 147 | +- `-Verify` reports a real verdict for all 111 verifiable features. |
| 148 | +- Exit code `2` continues to mean noncompliant, and `NotApplicable` never contributes to it. |
| 149 | + |
| 150 | +## 5. Track 3 — Integration tests |
| 151 | + |
| 152 | +`Tests/Integration/` has existed since July and is still empty. CI runs static validation |
| 153 | +and six unit files (470 lines total), none of which apply anything to a live system. |
| 154 | + |
| 155 | +### 5.1 Scope |
| 156 | + |
| 157 | +Windows Sandbox is the right harness: disposable, scriptable, present on Windows 11 Pro, |
| 158 | +and it discards state on close so a destructive test cannot damage the host. |
| 159 | + |
| 160 | +Target scenarios, in priority order: |
| 161 | + |
| 162 | +1. Apply a small registry-only feature set, verify with `-Verify`, assert exit code `0`. |
| 163 | +2. Apply, then undo, then assert the system returns to the pre-apply state. |
| 164 | +3. Inject a registry import failure, assert rollback triggers and restores. |
| 165 | +4. Inject an app removal failure, assert rollback does **not** trigger. |
| 166 | +5. `-WhatIf` over the full feature set mutates nothing. |
| 167 | + |
| 168 | +Scenarios 3 and 4 are the ones that give Track 1 its value — without them, auto-rollback |
| 169 | +is an untested claim. |
| 170 | + |
| 171 | +### 5.2 CI reality |
| 172 | + |
| 173 | +Windows Sandbox is not available on GitHub-hosted runners. Options, in order of preference: |
| 174 | + |
| 175 | +1. Keep integration tests **out** of the PR gate; run them on a self-hosted runner or |
| 176 | + manually before a release tag, and record the result in the release notes. |
| 177 | +2. Split the suite: the parts that need no live mutation (`-WhatIf`, exit codes, profile |
| 178 | + parsing) run in CI; the mutating parts stay manual. |
| 179 | + |
| 180 | +Do not block this track on solving CI. A manually run suite with a documented procedure is |
| 181 | +worth more than no suite. |
| 182 | + |
| 183 | +## 6. Track 4 — Field validation of the DISM fallback |
| 184 | + |
| 185 | +The wildcard `/PackageName` bug fixed in #9 was never observed failing; it was diagnosed |
| 186 | +from DISM's documented contract and the tell-tale tolerance of exit code 87. The fix is |
| 187 | +reasoned, not measured. |
| 188 | + |
| 189 | +Before tagging v3.4.0, on a real 24H2 machine with Copilot, Dev Home, or the new Teams |
| 190 | +still provisioned: |
| 191 | + |
| 192 | +1. Confirm `Get-AppxProvisionedPackage -Online` returns the expected package names. |
| 193 | +2. Run the fallback and capture the actual `$LASTEXITCODE`. |
| 194 | +3. Confirm the package is gone from the provisioned list afterwards. |
| 195 | +4. Record the result in `CHANGELOG.md`. |
| 196 | + |
| 197 | +If the fallback still fails, that is a v3.4.0 blocker, not a footnote — it is the path that |
| 198 | +exists specifically for the packages 24H2 is most aggressive about reinstalling. |
| 199 | + |
| 200 | +## 7. Explicitly out of scope |
| 201 | + |
| 202 | +Deferred so they are not relitigated mid-release: |
| 203 | + |
| 204 | +- **Localization.** Upstream PRs #764 and #643 and issue #499 all want it. The WinSwift GUI |
| 205 | + has diverged from upstream and its strings are hardcoded, so neither PR ports cleanly. |
| 206 | + This is its own release track. |
| 207 | +- **Preset library.** Only `gaming-rig.json` exists. Cheap and user-visible, but it is |
| 208 | + breadth, not recoverability. Candidate headline for v3.5.0. |
| 209 | +- **The deferred `ef8811d` port backlog.** Nine files listed in `UPSTREAM.md`. Note that |
| 210 | + `InvokeChanges.ps1` appears on both that list and Track 1 of this plan — see §8. |
| 211 | +- **New tweaks from upstream enhancement issues** (#560, #482, #343, #267). |
| 212 | + |
| 213 | +## 8. Sequencing and the one collision |
| 214 | + |
| 215 | +``` |
| 216 | +Track 2 (verification) ─┐ |
| 217 | +Track 3 (integration) ─┼─> Track 1 (rollback) ─> Track 4 (field validation) ─> tag |
| 218 | + ┘ |
| 219 | +``` |
| 220 | + |
| 221 | +Tracks 2 and 3 are independent and can run in parallel. Both should land **before** Track 1, |
| 222 | +not after: rollback is the change most likely to break something, and it should be built |
| 223 | +against a test suite and a verification engine that can actually catch the breakage. |
| 224 | + |
| 225 | +**The collision:** Track 1 rewrites `InvokeChanges.ps1`, which is also on the deferred |
| 226 | +upstream port list in `UPSTREAM.md`. Decide before starting whether to port upstream's |
| 227 | +error handling for that file first or to write the rollback wiring against the current |
| 228 | +code and re-reconcile afterwards. Doing both independently will produce a conflict that is |
| 229 | +harder to resolve than either change alone. Recommendation: write rollback against the |
| 230 | +current code, then reconcile once, with the integration suite from Track 3 as the safety |
| 231 | +net. |
| 232 | + |
| 233 | +## 9. Open decisions |
| 234 | + |
| 235 | +These need answers before implementation, not during: |
| 236 | + |
| 237 | +1. The failure policy table in §3.2 — specifically whether a registry import failure should |
| 238 | + prompt or roll back silently in interactive mode. |
| 239 | +2. The new exit code value for "failed and rolled back". |
| 240 | +3. Whether integration tests gate releases or merely inform them (§5.2). |
| 241 | +4. The `InvokeChanges.ps1` ordering question in §8. |
0 commit comments