Skip to content

feat(debloat): allow-listed Appx removal with per-package results - #45

Merged
chrystiamjr merged 4 commits into
mainfrom
feat/debloat
Aug 21, 2026
Merged

feat(debloat): allow-listed Appx removal with per-package results#45
chrystiamjr merged 4 commits into
mainfrom
feat/debloat

Conversation

@chrystiamjr

Copy link
Copy Markdown
Owner

Closes E4 — Debloat (E4-01E4-04). Curated, allow-listed removal of preinstalled apps, behind a restore point, with per-package results.

Draft: E4 has not been exercised on hardware. See Before this merges below.

The allow-list is a translation, not a filter

The obvious shape is "validate the package name the frontend sent". This does not do that.

A DebloatEntry carries an id of the app's own making — weather, xbox, oneDrive — and keeps the real Appx family names (Microsoft.BingWeather, …) inside DebloatCatalog.cs. The IPC payload only ever names ids; the removal command only ever names catalog constants. There is therefore no string a payload can carry that reaches a command — the worst a malformed one can do is fail to match an id.

That closes the near-match class by construction rather than by pattern. Submitting the real package name is rejected exactly like a wildcard is, and a Cypress spec asserts the same thing from the other side: no package family name appears anywhere in what the frontend puts on the wire.

It also solved a problem the Tweak catalog does not have — a dotted package name cannot be an i18n path segment, because rosetta splits on dots.

Not a Tweak, and never presented as one

An uninstalled app cannot be put back by this app, only reinstalled from the Store by the user. So Debloat is deliberately not a desired-state screen the way Optimize is — there is no un-remove to express. A tick means "uninstall this". Nothing starts ticked, Optional sits in its own warned section, and the confirmation names every app plus how each one would have to come back.

SafetyCheckpoint is extracted from TweakEngine rather than copied. Debloat needs the restore point more than a Tweak batch does, since it is the only way back at all — and a safeguard that exists on one of the paths that need it is a failure this project has already had once. Its log keys moved to log.checkpoint.*, and the shared preference now lives on the shared object.

Outcomes are read off the machine

Remove-AppxPackage can decline a package Windows considers in use and still leave a zero behind it — the same lesson Checkpoint-Computer taught, applied before it could bite. The batch re-reads the installed set afterwards and reports each entry by what it finds.

The second commit fixes a defect the Spec review found in that guard: the read returned an empty set when the query failed, which is the safe direction on the load path but the exact opposite on the read-back, where it would have reported every selected app as removed on the strength of a query that never ran. It was not even expressible in the harness — the fake returned success for every scripted answer — so FailingCapturesByArgs now tells "the machine said nothing" from "the query did not run", and the read is tri-state.

Three things must now be true before an app may be called removed: a removal was really issued for it, the verification read really answered, and the app is not in what it answered. The first clause also closes a quieter hole around OneDrive, whose absence marker reads identically whether this app removed it or it was never installed there at all.

Validation

Before After
xUnit 266 324
Vitest 281 316
Cypress 110 131

npm run validate and dotnet build clean.

Four guards were proven by injecting the regression each targets, per .agents/rules/falsifiable-test-guards.md — the mutation lock, the near-match id rejection, both new i18n copy guards, and the measured action bar (dropping mt-auto leaves 428px and 586px of padding below it, and the guard catches both).

Deliberate deviations

  1. The debloatLoaded payload carries no label/description keys, though E4-02's Scope lists them. Doing so would undo the invariant that makes the copy guard possible: no C# literal ties a row to its title, which is exactly why i18n.test.ts reads the ids out of the catalog source.
  2. RunDebloat hand-rolls its refresh instead of calling MutateAndRefresh. So does ApplyTweaks, for the same reason — both refresh conditionally on whether the batch really ran. ARCHITECTURE.md's "refresh only after success" was amended to describe what both actually do.
  3. No provisioned-package removal and no in-app reinstall, per E4-01's Scope.
  4. Xbox is one row, not twoMicrosoft.GamingApp and Microsoft.Xbox.TCUI are one decision to the user.

Before this merges

  • Exercise on hardware under elevation. The two things to watch are Get-AppxPackage's real output shape, and whether Remove-AppxPackage really does exit 0 on a package it declined — the read-back exists for exactly that, and hardware is where it gets proven.
  • Merging publishes: semantic-release will derive the version from these commit types, build, tag, and attach the installer to a public Release.

🤖 Generated with Claude Code

chrystiamjr and others added 4 commits August 20, 2026 22:20
Debloat is deliberately not a Tweak: an uninstalled app cannot be put
back by this app, only reinstalled from the Store. So it gets its own
boundary rather than a reversible one it could not honour.

The allow-list is a translation, not a filter. A DebloatEntry carries an
id of our own making (weather, xbox, oneDrive) and keeps the real Appx
family names inside DebloatCatalog.cs, so the IPC payload only names ids
and the removal command only names catalog constants. No string a
payload can carry reaches a command, which closes the near-match class
by construction; it also keeps ids usable as i18n path segments, which a
dotted package name is not.

SafetyCheckpoint is extracted from TweakEngine rather than copied, so
the restore point cannot exist on only one of the paths that need it —
Debloat needs it most, being the one action with no in-app undo. Its log
keys move to log.checkpoint.*, and the preference now lives on the
shared object.

Per-entry outcomes are read back off the machine: Remove-AppxPackage can
decline a package Windows considers in use and still exit 0, the same
lesson Checkpoint-Computer taught, applied before it could bite.

The screen is not a desired-state model — there is no un-remove to
express. Nothing starts ticked, Optional sits in its own warned section,
and the confirmation names every app and how each would come back.

321 xUnit, 316 Vitest, 127 Cypress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The read-back was right; what it read through was not.
ReadInstalledPackageNames returned an empty set when the query failed.
On the load path that is the safe direction — the UI then offers nothing
to remove. On the read-back after a removal the same answer means the
opposite, and every selected app would have been reported as removed on
the strength of a query that never ran.

It was not expressible in the harness either: FakeCommandRunner returned
Success: true for every scripted answer, so "the machine said nothing"
and "the query did not run" were one string. FailingCapturesByArgs tells
them apart, and the read is tri-state now.

Three things must be true before an app may be called removed: a removal
was really issued for it, the verification read really answered, and it
is not in what that answer contained. The first clause also closes a
quieter hole around OneDrive, whose absence marker reads the same
whether this app removed it or it was never installed at all.

From the Standards axis: Debloat's action bar is a byte-identical copy
of Optimize's and had inherited its fix but not its measured guard.
Both bars are measured now — dropping mt-auto leaves 428px of padding
below the bar, which the guard catches. The raw rgba that copy carried
across is a shadow-action-bar token, in both pages.

324 xUnit, 316 Vitest, 131 Cypress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both branches invented a SafetyCheckpoint independently, which is the
strongest evidence the extraction was right — and #46's is the better
one. It also owns the maintenance registry backup, keeps the log.tweaks.*
keys, and exposes CreateSafetyBackupEnabled as a forwarding property that
ActionRegistry.SaveSettings already writes to. Taken wholesale; the
log.checkpoint.* rename this branch carried is dropped, in code, locales
and tests.

Debloat routing moved from ActionHost into ActionRegistry, where routing
now lives: ActionHost keeps concurrency, dispatch and actionFinished, per
ADR 0009. loadDebloat, runDebloat, the mutation entry and PushDebloat are
re-expressed there unchanged.

DebloatManager shares the window's checkpoint with Optimizer, so the
"create a safety backup" preference reaches it through the object
SaveSettings already writes to rather than a second instance.

Frontend: sendAction is typed against SystemActionPayloads now, so both
Debloat actions are declared there — runDebloat carries catalog ids,
never Appx family names. App.tsx keeps the per-run debloatResults state
alongside the useHostMutation/useLogBuffer split.

346 xUnit, 327 Vitest, 131 Cypress; validation ran green before this
commit, which skipped the hook only because commitlint rejects "merge:".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chrystiamjr
chrystiamjr marked this pull request as ready for review August 21, 2026 06:44
@chrystiamjr
chrystiamjr merged commit b0bb69f into main Aug 21, 2026
3 checks passed
@chrystiamjr
chrystiamjr deleted the feat/debloat branch August 21, 2026 17:22
chrystiamjr pushed a commit that referenced this pull request Aug 21, 2026
## [0.4.0](v0.3.0...v0.4.0) (2026-08-21)

### Features

* **debloat:** allow-listed Appx removal with per-package results ([#45](#45)) ([b0bb69f](b0bb69f))
@chrystiamjr

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 0.4.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant