diff --git a/App.xaml.cs b/App.xaml.cs
index ab1345a..95aa7bd 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -1,6 +1,5 @@
using System.Windows;
using System.Windows.Threading;
-using Velopack;
using ytLive.Helpers;
namespace ytLive;
@@ -31,19 +30,8 @@ public partial class App : Application
// the per-build GUID is forensic only and lands here.
AppLog.Write($"Build {ytLive.Helpers.BuildStamp.Id} (compiled {ytLive.Helpers.BuildStamp.BuiltLocal})");
- // Velopack auto-update bootstrap — checks for updates on startup and
- // applies them (the app restarts automatically if an update is found).
- // Update URL is configured per-deployment (self-hosted on DO droplet);
- // when no URL is set, the check is a no-op.
- try
- {
- VelopackApp.Build()
- .Run();
- }
- catch (Exception ex)
- {
- AppLog.Write($"Velopack bootstrap failed (non-fatal): {ex.Message}");
- }
+ // There is deliberately NO self-update bootstrap here: the Microsoft Store updates the
+ // app (TASK 48 — Store MSIX route), and a second updater would fight it.
base.OnStartup(e);
AppLog.Write("App OnStartup end");
diff --git a/Distribution.md b/Distribution.md
index 62d0738..c5cf691 100644
--- a/Distribution.md
+++ b/Distribution.md
@@ -409,10 +409,12 @@ Set-AuthenticodeSignature -FilePath "LlamaCasty.exe" `
### 4.4 Update Mechanism — ⛔ OBSOLETE (2026-09-27)
> **Superseded by Store updates.** Microsoft delivers and updates the app from the listing, so
-> there is nothing to check and no manifest to host. The Velopack updater currently in
-> `App.xaml.cs` **downloads *and executes***, which Store policy **10.2.2** does not permit — its
-> removal is a **compliance action**, not optional cleanup (TASK 48 item 3). The Polar-native design
-> below is retained as a record of the approach that was rejected; it is not an option.
+> there is nothing to check and no manifest to host. The Velopack updater that used to sit in
+> `App.xaml.cs` **downloaded *and executed***, which Store policy **10.2.2** does not permit — its
+> removal was a **compliance action**, not optional cleanup, and it is **done** (TASK 48 slice 3,
+> 2026-10-22: the package reference and the bootstrap are gone; `ytLive.Tests/StorePackagingTests.cs`
+> guards it). The Polar-native design below is retained as a record of the approach that was
+> rejected; it is not an option.
**Polar-native approach (no custom infrastructure):**
diff --git a/HANDOFF.md b/HANDOFF.md
index b99f33a..900dafa 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -77,6 +77,16 @@
unchanged; the seams are two `internal` properties). Guard:
`ytLive.Tests/AppLogRotationTests.cs`. Slices 2 (temp alert-clip cleanup) and 3 (low-disk
guard before recording) are still open.
+- **TASK 48 slice 3 — the self-updater is gone (shipped 2026-10-22).** The Store route (creator
+ ruling 2026-09-27) means the Store updates the app, so the Velopack package + bootstrap were
+ **deleted** (`ytLive.csproj`, `App.xaml.cs`) — a compliance action, not cleanup: the updater
+ downloaded *and executed*, which Store policy 10.2.2 does not permit. The DO update-URL droplet
+ dies with it. Guard: `ytLive.Tests/StorePackagingTests.cs` (no updater package, no updater API
+ in any app source file). The app was smoke-tested after the removal: `MainWindow loaded`, no
+ exceptions. **Still open on TASK 48:** slice 2 (the `Package.appxmanifest` + packaging project),
+ slice 6 (the full-trust recording invariant comment), slice 7 (disqualifier re-check), slice 8
+ (the pre-submission gates) — and slices 4/5 are the creator's (Certification Kit, Partner
+ Center listing, the demo account).
- **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home
Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan
was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and
@@ -270,10 +280,10 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
Galaxy `cef.log` at 2.7 GB is NOT ours.)
## Tests
-- Last full run: **436/436 passed, 0 warnings** (2026-10-22 — after TASK 55 slice 1's
- `AppLogRotationTests`; the run before was 435/435 after TASK 63, and the runs before that were
- 435/435 after TASK 61+62, 433/433 after TASK 60, 432/432 after TASK 59 and 431/431 after
- TASK 58). The one failure seen earlier in the
+- Last full run: **437/437 passed, 0 warnings** (2026-10-22 — after TASK 48 slice 3's
+ `StorePackagingTests`; the run before was 436/436 after TASK 55 slice 1's
+ `AppLogRotationTests`, and the one before that 435/435 after TASK 63). The one failure seen
+ earlier in the
session was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
diff --git a/TASKS.md b/TASKS.md
index 8b08aec..d234957 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -63,7 +63,7 @@
| 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) |
| 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) |
| 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | ✅ Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) |
-| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28** | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
+| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** — the Store updates the app | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
| 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Creator check 2026-10-09: **Remove jitter FIXED** (temporal mask smoothing 0.75/0.25, OBS/Jitsi-derived) and **Blur "heavy" FIXED** (quadratic radius 1→6 small px, default 0.5 = 2, StreamFX-consensus); recheck on the real camera pending | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
@@ -129,7 +129,7 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw
`DroppedFrames`/`StreamDuration`; `SessionTeardownTests` is the natural home for the roll-up.
- **TASK 3** — items 16 (Text source) + 20 (RewardEvent capture → SQLite, Alerts' persistence half) open; **17 (Alerts) is DONE via TASK 43 (2026-09-24) — native six-event alert box**
- **TASK 9** — item 5 open (webcam identity key reconciliation)
-- **TASK 10** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates)
+- **TASK 10** — the Velopack update URL is **RETIRED (2026-10-22)**: the updater was deleted (TASK 48 slice 3) and the Store updates the app
- **Shipping / release build (creator-queued 2026-09-26)** — no publish config exists yet:
the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish`
is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled).
diff --git a/TASKS/task-10-monetization.md b/TASKS/task-10-monetization.md
index 04700a5..c908e12 100644
--- a/TASKS/task-10-monetization.md
+++ b/TASKS/task-10-monetization.md
@@ -68,7 +68,7 @@ licensing path is being deleted because Store IAP supplies the same bit from the
handles purchase. Paid users see zero license UI.
7. ⛔ **Licensing model / renewal-lapse path** — already dead before the Store ruling (a perpetual
key never expires). Now superseded again: **Store IAP has no keys at all.**
-8. 🔶 **Velopack auto-updates** — NuGet packages + bootstrap in `App.xaml.cs`. **TEARDOWN is a
- compliance action, not cleanup:** the updater downloads *and executes*, and Store policy 10.2.2
- does not permit it. Tracked as [`TASKS/task-48-distribution-msix.md`](task-48-distribution-msix.md)
- item 3; the Store updates the app instead.
\ No newline at end of file
+8. ✅ **Velopack auto-updates** — NuGet package + bootstrap in `App.xaml.cs`. **DELETED
+ 2026-10-22 (TASK 48 slice 3)** — a compliance action, not cleanup: the updater downloaded
+ *and executed*, and Store policy 10.2.2 does not permit it. The Store updates the app
+ instead. Guard: `ytLive.Tests/StorePackagingTests.cs`.
\ No newline at end of file
diff --git a/TASKS/task-48-distribution-msix.md b/TASKS/task-48-distribution-msix.md
index c2d7977..ec8f349 100644
--- a/TASKS/task-48-distribution-msix.md
+++ b/TASKS/task-48-distribution-msix.md
@@ -158,15 +158,21 @@ There is currently **no `.manifest` file in the repo at all**, so this is purely
**Do NOT use `appContainer`** — see the invariant in item 6.
-## 3. ☐ Remove Velopack — 3 edit sites
+## 3. ✅ Remove Velopack — 3 edit sites (DONE 2026-10-22)
The Store handles updates, so this is simply **deleted**. Trivially removable; the code was
written defensively for exactly this.
-- `ytLive.csproj:92` — ``
-- `App.xaml.cs:3` — `using Velopack;`
-- `App.xaml.cs:38-46` — the sole call site, already try/caught and commented
- "(non-fatal) … when no URL is set, the check is a no-op"
+- `ytLive.csproj:92` — `` — **REMOVED**
+- `App.xaml.cs:3` — `using Velopack;` — **REMOVED**
+- `App.xaml.cs:38-46` — the sole call site (already try/caught and commented
+ "(non-fatal) … when no URL is set, the check is a no-op") — **REMOVED**
+
+**Guard:** `ytLive.Tests/StorePackagingTests.cs` — the ONE test asserts the app project
+references no updater package (Velopack/Squirrel/NetSparkle/AutoUpdater/WinSparkle) and that no
+app source file uses the updater API (matched as CODE — a `using` directive, a type usage — not
+as a bare word, so a file's prose may still name what it deliberately does not do). The app was
+smoke-tested after the removal: `MainWindow loaded`, no exceptions.
**Retires:** the pending "Velopack update URL" item (`TASKS.md` open items,
`task-10-monetization.md:43`) and the self-hosted DigitalOcean droplet.
diff --git a/ai.md b/ai.md
index e7e50e9..acb61e0 100644
--- a/ai.md
+++ b/ai.md
@@ -2583,8 +2583,10 @@ before re-deriving it. Executable checklist: `TASKS/task-48-distribution-msix.md
**Consequences that are architecture, not packaging detail:**
-- **⛔ Velopack and the update URL are deleted** — the Store auto-updates. 3 edit sites,
- `ytLive.csproj:92` + `App.xaml.cs:3,38-46`. The self-hosted DO droplet dies with it.
+- **⛔ Velopack and the update URL are deleted** — the Store auto-updates. **DONE 2026-10-22**
+ (TASK 48 slice 3): the `PackageReference` in `ytLive.csproj` and the bootstrap in
+ `App.xaml.cs` are gone, and `ytLive.Tests/StorePackagingTests.cs` guards that no updater
+ package or updater API comes back. The self-hosted DO droplet dies with it.
- **⛔ The Polar licensing subsystem is deleted** — `PolarLicenseService`, `PolarLicense`,
`MainViewModel.License.cs` (`PremiumUrl`, customer portal, the
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy), and the
diff --git a/ytLive.Tests/StorePackagingTests.cs b/ytLive.Tests/StorePackagingTests.cs
new file mode 100644
index 0000000..6d1a4b9
--- /dev/null
+++ b/ytLive.Tests/StorePackagingTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Xunit;
+
+namespace ytLive.Tests;
+
+/// TASK 48, slice 3 — the creator's ruling (2026-09-27): the app ships as a
+/// **Microsoft Store MSIX** and the **Store updates it**. The self-update subsystem that
+/// existed for the abandoned self-hosted route (a Velopack bootstrap plus the DigitalOcean
+/// update-URL droplet) is therefore deleted, not parked: a second updater inside a
+/// Store-installed package fights the Store's own updater.
+///
+/// This test is the guard: the app project references no updater package and no app
+/// source file uses the updater API — so the deleted subsystem cannot creep back in, and
+/// the "the Store updates it" decision stays the only update path.
+public class StorePackagingTests
+{
+ /// Known self-update packages. If one of these comes back, the Store route has
+ /// been quietly reversed.
+ private static readonly string[] UpdaterPackages =
+ {
+ "Velopack", "Squirrel", "NetSparkle", "AutoUpdater", "WinSparkle"
+ };
+
+ [Fact]
+ public void The_app_ships_no_self_update_subsystem()
+ {
+ // 1. The app project references no updater package.
+ var project = XDocument.Load(SourceAssert.RepoPath("ytLive.csproj"));
+ var packages = project.Descendants()
+ .Where(e => e.Name.LocalName == "PackageReference")
+ .Select(e => (string?)e.Attribute("Include"))
+ .Where(name => name != null)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var updater in UpdaterPackages)
+ Assert.DoesNotContain(updater, packages);
+
+ // 2. No app source file uses the updater API. Matched as CODE (a using directive,
+ // a type usage), not as a bare word — a file's prose is allowed to name what it
+ // deliberately does not do.
+ var root = Path.GetDirectoryName(SourceAssert.RepoPath("ytLive.csproj"))!;
+ foreach (var file in AppSourceFiles(root))
+ {
+ var text = File.ReadAllText(file);
+ Assert.False(
+ Regex.IsMatch(text, @"^\s*using\s+Velopack\s*;", RegexOptions.Multiline),
+ $"{Path.GetFileName(file)} still imports the Velopack updater — the Store updates this app.");
+ Assert.False(
+ Regex.IsMatch(text, @"\bVelopackApp\b"),
+ $"{Path.GetFileName(file)} still calls the Velopack updater — the Store updates this app.");
+ }
+ }
+
+ /// Every C# file of the app project (not the test project, not build output).
+ private static IEnumerable AppSourceFiles(string root)
+ {
+ return Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories)
+ .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}ytLive.Tests{Path.DirectorySeparatorChar}"))
+ .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}"))
+ .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}"));
+ }
+}
diff --git a/ytLive.csproj b/ytLive.csproj
index 585eb94..015f5e3 100644
--- a/ytLive.csproj
+++ b/ytLive.csproj
@@ -125,7 +125,6 @@
-