docs: bank the Windows Store + signing research, and fix the dead EV-certificate line

The distribution answer existed only in conversation, so every session re-derived
it. It is now in the map, and the route decision is explicitly parked as the
creator's.

new TASKS/research-store-certification.md — Store Policies 7.20 + MSIX packaging:
which policies bind, which don't (and why), cert economics, camera/mic gating
layers, YouTube age + COPPA, the 11.12 UGC judgment call.

Two real defects surfaced, neither fixed (docs-only unit):
- FfmpegLocator downloads an unsigned exe from GitHub and runs it. That is
  policy 10.2.2 (dynamic code inclusion) verbatim, and it is the root cause of
  the 2026-09-01 404 — the pin aged out of BtbN's 14-day retention on the
  creator's first real recording attempt. -> TASK 48 item 1, not
  Store-conditional.
- Distribution.md:318 recommended a $400+/yr EV cert for a SmartScreen bypass
  Microsoft removed in March 2024. Fixed; had it shipped it would have cost
  $400+/yr to buy what $150 buys.

Also new: TASKS/task-48 (checklist, carved out of TASK 36 item 6) and
TASKS/task-49 (chat profanity filter, not blocked). ai.md gains the durable
invariants — full trust or recording breaks silently, chat is rendered never
stored — plus a correction to the FFmpeg locator section. MyMistakes.md records
the lesson: a policy citation is a claim about scope, not just text.

MARCOM.md got the privacy-copy guard but is gitignored by design, so that edit
stays local and did not travel here.
This commit is contained in:
2026-09-27 14:17:13 -07:00
parent 938c5b3de4
commit e78c58fc28
8 changed files with 1044 additions and 128 deletions
+30 -1
View File
@@ -315,7 +315,36 @@ Single-file means everything is inside the `.exe`. No DLLs, no runtime dependenc
### 4.3 Code Signing
**Certificate:** Buy an EV (Extended Validation) code-signing certificate from DigiCert or Sectigo ($200-400/year). EV certificates get instant SmartScreen reputation; DV/OV certs trigger SmartScreen warnings for months.
> ⚠️ **The cert choice below is a live decision, and the old plan's EV recommendation is
> dead.** Microsoft **removed the SmartScreen instant-bypass for EV certificates in March
> 2024** — an EV-signed file now accrues reputation exactly like an OV-signed one, so paying
> $400+/yr buys precisely what paying ~$150 buys. **Do not buy EV.**
>
> **But the more interesting finding is that you may not need a certificate at all.** Three
> costs, current as of 2026-09-27:
>
> | Route | Cert/yr | SmartScreen | Note |
> |---|---|---|---|
> | **Microsoft Store (MSIX)** | **$0** — Microsoft re-signs | **No warning** | Requires MSIX packaging; Store review; public listing |
> | **Microsoft Store (EXE/MSI)** | $120–300 — *your* cert | Warning ramp | Store does **not** re-sign; silent install mandatory. Strictly dominated — cert required anyway |
> | **Direct + OV cert** | $150–300 | Warning ramp | Worldwide availability |
> | **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120 | Warning ramp | **Individuals: USA/Canada only.** Identity validation required |
>
> Signing does **not** clear the warning on day one either way: a valid cert stops the
> *malware* warning immediately, but "unrecognized publisher" clears as SmartScreen accrues
> reputation from real download volume. Budget cert validity at **460 days** (CA/B Forum
> CSC-31) — this is a recurring line item, not a one-time purchase — and note that private
> keys must sit on an **HSM or hardware token**.
>
> **The full comparison, the four real combinations, and the policy analysis live in
> [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) §2–3.
> The executable checklist is [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md),
> and the route decision is the creator's.**
**Certificate:** Buy an **OV (Organization Validation)** code-signing certificate from DigiCert
or Sectigo (~$150–300/year), or an **Azure Artifact Signing** profile (~$9.99/month,
USA/Canada individuals only) if the geography allows. ⛔ **Not EV** — no SmartScreen benefit
since March 2024. **Or buy nothing at all** if the route is the Microsoft Store.
**Signing process:**
```powershell
+107 -126
View File
@@ -1,146 +1,127 @@
# HANDOFF — current state
**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`.
**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main`
(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so.
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
**Last pushed:** `938c5b3` (alert ticker). This unit (distribution/certification research) is
**docs-only** — no code touched, no build, no tests run.
## The 2026-09-26 proof-of-concept round (creator reporting)
## This unit: distribution & Store certification research is WRITTEN DOWN
Context: **no YouTube private test recordings are being saved**, so the creator is judging
compositing from **local recordings**. They assumed the live path needed a separate "redirect" —
**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the
`MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68`
(go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two
outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch".
The session's open question was "how do we get this in front of users without a SmartScreen
scarewall" and it had been answered **in conversation only** — which meant the next session
would re-derive it. It is now in the map, and the conversation can be dropped.
### Done: the alert ticker draws inside the Stream Alerts box
| File | What it is |
|---|---|
| `TASKS/research-store-certification.md` | **new** — the authoritative research. Store Policies **7.20** (effective 2026-10-22, re-check the version), MSIX full-trust vs AppContainer, cert economics, a ⛔/✅ table of which policies bind and which don't, the camera/mic gating layers, the YouTube age + COPPA analysis, the 11.12 UGC judgment call, and the filter design constraints |
| `TASKS/task-48-distribution-msix.md` | **new** — the executable checklist. Carved out of TASK 36 item 6 (code signing / installer / Velopack URL) so distribution has one owner |
| `TASKS/task-49-chat-profanity-filter.md` | **new** — the chat filter checklist. Not blocked |
| `Distribution.md` §4.3 | **corrected** — the EV recommendation was dead (§ below) |
| `ai.md` | new "Windows packaging & distribution" section (durable invariants) + a correction on the FFmpeg locator |
| `MyMistakes.md` | the policy-applicability lesson |
| `TASKS.md` | rows 48/49, a research index, and the TASK 36 item 6 split |
| `MARCOM.md` | ⛔ **privacy-copy guard — gitignored, so this edit is local-only and did not travel with the push** |
Was a **global 1920px bar pinned to the top edge**; the creator wants it "over the stream alerts
video, not over the entire preview window". The strip is now rendered at the alert box's own size
and the box's origin travels on the frame in a new `VideoFrame.Placement` (`(int X, int Y)?`, with
`OriginX`/`OriginY` defaulting to 0 so full-canvas overlays are unaffected). Every blit site now
reads `tickerFrame.OriginX/OriginY` instead of a literal `0, 0` — `SceneCompositor` ×2 plus
`FramePump`'s static-bake `Overlay` path — and `PreviewPane.xaml`'s `AlertTickerElement` binds the
same rect (`AlertTickerLeft/Top/Width/Height`), so preview and output can't drift.
### Two real defects the research surfaced (both now recorded, neither fixed — no code this unit)
Design notes: the position rides on the frame rather than in a full-canvas frame because a
1920×1080 overlay is 8.3MB of LOH garbage per tick (~2.5GB over one 10s alert); the strip is
~370KB. `CopyStrip` now clips **rows** to the target height — the pill rasterises at 48px, so a
shorter box would have written past the buffer. **No alert box in the scene ⇒ no ticker.**
1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** That is Store
policy **10.2.2** (dynamic code inclusion) verbatim, *and* it is the root cause of the
2026-09-01 failure: the previous daily pin aged out of BtbN's 14-day retention and the
download **404'd on the creator's first real recording attempt**. `FfmpegLocator.cs:30-35`
records the incident but still reads like a design choice. → **TASK 48 item 1.** Not
Store-conditional: bundling kills this whole class of cold-start failure and deletes the
startup network dependency. **This is the highest-value unblocked item in the repo.**
2. **⛔ `Distribution.md:318` recommended a $400+/yr EV certificate for a benefit Microsoft
deleted in March 2024.** Fixed. Had it shipped, it would have cost $400+/yr to buy exactly
what $150 buys. Lesson in `MyMistakes.md` — the recurring shape is *citing a policy or a
platform fact from memory instead of re-verifying it in the document itself.*
### Done: the branding flash is no longer a go-live-only behaviour
### ⛔ The decision that is NOT made, and belongs to the creator
The presenter was `Start()`/`Stop()`-ed from `UpdateLiveVisuals()`'s `IsLive` branch, so a
recording made **without ever going live** carried no credit — exactly the creator's complaint.
It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it
runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the
recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes
`BrandFlashPresenter.Enabled`.
**The distribution route.** Research is done and MSIX is the front-runner (full trust is viable;
we trip **none** of the four MSIX disqualifiers — no driver installed, no per-user service, no
elevation, no shell extension). Four real combinations, costed in
`TASKS/research-store-certification.md` §3:
**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`.
When `Start()` was per-go-live, the next go-live restarted it; with one app-lifetime `Start()`
nothing would, so a key entered mid-session would leave the credit dead until the process
restarted. The setter now restarts the timer when re-enabling while `_running`.
| # | Route | Cert/yr | SmartScreen | Notes |
|---|---|---|---|---|
| A | Store MSIX + Store IAP | **$0** | none | Polar gets deleted; revenue cut; public listing |
| B | Store MSIX + **Polar** | **$0** | none | free signing **and** keep 100% — two systems to maintain |
| C | **Polar file hosting** + own cert | $120–300 | warning ramp | **the status quo already sketched in `Distribution.md:14`/`:296`** |
| D | Store EXE (policy 10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway, silent install, own URL |
Test-arithmetic trap, hit twice now: `Advance(5.1)` in one call can never observe a credit —
`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at
1/30s like the real timer (the `Step` helpers).
**Nothing else was allowed to block on this** — the user is right that most of the research is
route-independent and worth banking regardless. So: research banked, dead line fixed, and only
the packaging work is parked.
### Done this unit: recording save dialog — Cancel now discards
**Also still open, deliberately:** pricing (one-time vs one-time+monthly), the license provider,
and therefore all licensing copy. `OverlayHost.xaml` still claims Polar unlocks alerts — wrong,
alerts are not gated. Unresolved, queued, **not** to be papered over.
`RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`).
**The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true
and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording
under the default name. Extracted the decision into internal
`MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`:
### The two findings most likely to be forgotten
- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left
empty. A failed delete returns `DiscardFailed` and the toast names the file + folder.
- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name.
Blank box still means "keep the auto name" — but only on an explicit Save.
Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the
headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged".
Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect).
## Two earlier work units (committed)
### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped)
It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a
credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to
**both** the frame pump and the preview, so they cannot drift:
- `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue
halo, channels split opposite), 2s envelope, random on-canvas placement per presentation,
licence gate re-checked on every read.
- `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump +
`PublishBrandFlashPreview()` for the pane.
- `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the
C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path.
- `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame).
- `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable.
Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every
`rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas.
### 2. Dev-only: two instances side by side
`Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=<id>` and that process gets a private
`%APPDATA%\ytLlive\instances/<id>/` root for the **layout DB, auth file, startup.log and the
WebView2 user data folder**. Chromium locks that folder exclusively — without this the second
instance does not start at all. Recording folder and the ffmpeg `tools` cache stay shared on
purpose; global hotkeys stay un-namespaced.
**Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run`
The entire implementation is inside `#if DEBUG`. Release compiles to `DataRoot => DefaultRoot` +
`WebViewDataFolder => null`; the call sites are unconditional so Release cannot drift.
## Bugs found and fixed along the way
- **Pre-existing, not mine:** `FramePump`'s **fully-static shortcut** stretched the cached bake and
returned it, silently discarding **every per-frame overlay** — the social bar and the alert ticker
were already lost there, not just the flash. Now composites overlays onto a copy before stretching.
- **Cadence bug caught by my own test:** the interval was an absolute deadline, not a period, so
gaps collapsed (6.7s / 3.5s / 2.8s instead of 30–60s). Fixed.
- `BrandFlashPresenter` recycles one 8MB master buffer and **must** stamp `VideoFrame.Epoch` per
read, or the paste cache freezes the credit. It does.
## Test state — VERIFIED
- **357 total, 356 pass.** 18 new facts (10 brand flash, 8 instance isolation).
- The one failure is `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
and it is **flaky by environment, not a regression** — 2 fail / 1 pass in isolation. Its own doc
comment: "Requires an interactive desktop session: if the window is covered or the session is
locked, the pointer no-ops and the drag never lands." The symptom matches exactly (order
unchanged). **Run the suite with ytLive CLOSED** (see `MyMistakes.md` 2026-08 era note).
- `RealAppHost.RunAsync` was added for this: a frame-pump test MUST `await` inside it. A blocking
wait in `RealAppHost.Run` occupies the one shared STA thread and hangs the whole suite with no
output. **Always background a `vstest` run and poll the log** — foreground piped `vstest` returns
nothing in this shell even on success.
- **Distribution and licensing are independent.** Policy 10.8.1 lets a **Store-distributed**
app verify licenses with **Polar**. So "should we use the Store?" does not imply "drop Polar?"
Only route A removes Polar, and that is a licensing decision riding on a distribution one.
- **⛴ Two invariants that will break silently if touched:**
- **Full trust, or recording breaks.** `DefaultRecordFolder()` writes to Downloads/MyVideos;
that passes through unvirtualized **only** at `mediumIL`. Flip the manifest to
`appContainer` and output vanishes with no error. A comment is owed there **when the
manifest lands** (TASK 48 item 6) — not before.
- **Chat is rendered, never stored.** That non-persistence half is the load-bearing part of
the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload
may be added without re-arguing 11.12 first.
## Landmines
- A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027.
Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second.
Left alone deliberately.
- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued.
- **Stale fact inside `TASKS.md` (left alone, flagged here per the Scope Lock).** The "1.0 gates"
section asserts "**TASK 36 is now shipped**", but the catalog row 36 reads **☐ Queued** and
`task-36-gold-pass.md` still shows items 1–6 unchecked. The line most likely means *item 2*
(branding flash goes live) shipped at `9761b1d`. **Needs a one-line fix, not a code change** —
flagging rather than fixing because it is not this unit's job.
- **A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe`** and broke `dotnet run` with
MSB3027. Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
- **`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` is flaky by
environment** (needs an interactive desktop session; 2 fail / 1 pass in isolation). **Run the
suite with ytLive CLOSED.** Not a regression.
- **`RealAppHost.RunAsync` exists for a reason** — a frame-pump test MUST `await` inside it. A
blocking wait occupies the one shared STA thread and **hangs the whole suite with no output.**
Always background a `vstest` run and poll the log; a foreground piped `vstest` returns nothing
in this shell even on success.
- **`GlobalHotkeys`: two instances registering the SAME global hotkey** — Windows refuses the
second. Left alone deliberately.
- **MSIX writes under a real package identity are unverified.** The docs say full trust passes
user-profile writes through, but confirm on a real packaged build before trusting it with
recordings (TASK 48 item 6).
## Test state
**367/367 as of `938c5b3`.** Not re-run for this unit — docs only, no code touched.
## Uncommitted / untracked
- `MARCOM.md` — the privacy-copy guard was added but the file is **gitignored by design**
(confidential business file, `.gitignore:11`). It stays local, as intended. Same for
`MONETIZATION.md` and `CREDENTIALS.md` — **never commit those.**
## Next
1. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm
the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027
*before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2`
+ `dotnet run --no-build` in a second shell is the known-good path.
2. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health
message) when a stream or recording ends.
3. **The creator's own eyes on the output** — the ticker-placement and brand-flash changes are proven
by unit tests and the compositor, but the final proof is a local recording, since no YouTube
private test recordings are being saved.
4. Push the commits when the creator asks.
5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
input, Enter inserts a newline.
6. Decide the alert-gating copy question above.
7. Ship-checklist items live in `TASKS.md` → "1.0 gates".
1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, recommended on every route, and it fixes a
real user-facing failure. *This is the next code unit.*
2. **The route decision** (creator) — A/B/C above, once the ffmpeg fix is out of the way.
3. **Vertical recording verification** — the compositor tier is proven by
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
4. **Multi-instance** — route `FfmpegLocator._toolsDir` through `InstanceProfile` (same shared
extraction race as #1, and it becomes moot if ffmpeg is bundled). Confirm the launch method:
`$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path.
5. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health
message) when a stream or recording ends. `SessionTeardownTests` is the natural home.
6. **`scripts/publish.sh` + Debug-only `InternalsVisibleTo` + the `LlamaCasty.exe` rename** —
still queued from 2026-09-26; do **NOT** add `-p:PublishTrimmed=true` (WPF fails at runtime,
not build time). See `TASKS.md` → "Shipping / release build".
7. **The alert-gating copy** (`OverlayHost.xaml`) — fix the copy or gate the alerts; do not leave
it lying. Blocked behind the pricing ruling.
8. **The camera-privacy measurement** — does the Win11 desktop-app camera toggle actually gate a
DShow webcam and a capture card? Gates all privacy-forward copy (`MARCOM.md` guard).
9. **TASK 36:212 stale "shipped" line** — one-line fix, needs a hand.
10. Ship-checklist items live in `TASKS.md` → "1.0 gates".
+37
View File
@@ -1094,3 +1094,40 @@ the default file name and save the file — that's what pressing enter should do
the save option, discarding the recording." A Cancel button on a destructive-and-final dialog means
*discard*, not *accept default*. When a dialog is the last gate before data is committed, ask what
Cancel should destroy — don't infer "keep the default" just because the default already exists.
---
## 2026-09-27 — A policy number in my head is not a policy finding: check that the policy *applies*
Two confident, wrong claims in one Store-research pass, both of the same shape — I had a policy
number and a confident paraphrase attached to it, and never asked whether the policy's own scope
clause covered us.
**1. "Store policy 10.10 (Advertising) requires X"** — 10.10 does not apply to LlamaCasty at
all. Every clause in it is conditioned on **ads your product displays**. YouTube injects ads
*server-side after the RTMP handoff*; the app renders no ad, hosts no ad SDK, and makes no ad
decision. I spent a cycle designing around a rule that was never in force. Same error later with
**10.2.5** (Store-only installation): the current policy text scopes it to **game and Xbox
console products**, and the older v7.7 wording that said "all of your apps" has been narrowed —
citing the stale wording would have produced a confidently wrong conclusion in the *opposite*
direction (claiming we were locked to MSIX).
**2. "Buy an EV certificate — EV gets instant SmartScreen reputation."** This one was worse than
wrong, because it was already **written into `Distribution.md`** and would have cost $400+/yr.
Microsoft **removed the SmartScreen instant-bypass for EV certs in March 2024**. EV and OV now
accrue reputation identically. I recited a fact that had been dead for over two years, in a
document that already contained the recommendation.
**The rule, and the generalisation:** *a policy citation is a claim about scope, not just about
text.* Before acting on one, read the **applicability sentence** and answer three questions —
does it cover our product class, does it cover our distribution model, and is the wording the
**current** version? A policy number without a verified scope is a guess wearing a citation's
clothes, and the failure mode is invisible because the sentence still *sounds* authoritative.
Corollary: **a stale external fact inside a shipped document is worse than a missing one** — it
is trusted, acted on, and costed. When a doc makes a factual claim about the outside world
(certificates, platform behaviour, policy text), re-verify it on touch, not on memory. This is
the same class as the dead-Pin incident in `FfmpegLocator` (`ai.md` → FFmpeg locator): an
external thing changed, our record of it did not, and the mismatch was discovered by a user
rather than by a test. **Records of the outside world rot silently and are believed until they
cost money.**
+33 -1
View File
@@ -63,6 +63,17 @@
| 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: route decision, bundled ffmpeg, MSIX + code signing | ⏳ Queued — **blocked on the creator's route decision**; bundled-ffmpeg half is unblocked and recommended | [`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) |
---
## Research index
| Research | Covers |
|---|---|
| [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) | YouTube Live Streaming API v3 — authoritative facts for the v3 build |
| [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) | **Windows Store policies 7.20 + MSIX packaging + code-signing economics** (2026-09-27). Feeds TASK 48. Records which policies bind, which don't, and why — read it before re-litigating distribution |
---
@@ -104,7 +115,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
- **TASK 10** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates)
- **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).
@@ -198,6 +209,27 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw
**ticker** ("Funder — Super Chat · $10.00") scrolls along the very top of the frame
(never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings:
custom + fallback (not either/or), ticker on top, no ducking.
- **TASK 36 item 6 (release engineering) is now TASK 48** (2026-09-27) — code signing, the
installer, and the Velopack update URL have one owner instead of three scattered mentions.
**EULA draft/review and the THIRD-PARTY-NOTICES license-texts gate stay in TASK 36 item 6**,
as does the agreed build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only).
- **TASK 48 — distribution & packaging** — **⏳ the route decision belongs to the creator.**
Research is done and MSIX is the front-runner; the four real combinations (Store+Store IAP /
Store+Polar / Polar-hosted+own cert / dominated Store-EXE) and their cert costs are tabulated
in `TASKS/research-store-certification.md` §3. Two things are settled and unblocked:
**(a) bundle ffmpeg instead of downloading it** — `FfmpegLocator` currently downloads an
unsigned exe from GitHub and runs it, which is Store policy 10.2.2 (dynamic code inclusion)
*and* is the root cause of the 2026-09-01 expired-pin 404; **(b) the `Distribution.md` EV
claim is dead** — Microsoft removed the SmartScreen EV bypass in March 2024, so paying
$400+ buys what $150 buys. Full-trust MSIX is viable (mediumIL, not AppContainer, and the
three technical disqualifiers — drivers, per-user services, elevation — are all clear).
Packaging, Velopack removal, WACK, the demo account, the privacy policy and the IARC
questionnaire are all specified in the task file and waiting.
- **TASK 49 — chat profanity filter** — queued, not blocked, size S. Local, on-device,
**non-persistent**, opt-in, **user-supplied word list (never a hardcoded slur list)**, and it
must **never match the SuperChat amount or reward fields** (financial data, Store 10.5.5).
The non-persistence half is the same property that makes the 11.12 UGC certification answer
strong, so **no future chat-history/moderation-log/analytics feature may quietly break it.**
---
+448
View File
@@ -0,0 +1,448 @@
# ytLlive — Windows Store & code-signing certification research (authoritative)
> Catalog: [`TASKS.md`](../TASKS.md) · memory map: [`schema.md`](../schema.md) · distribution plan: [`Distribution.md`](../Distribution.md)
>
> **Researched 2026-09-27** against **Microsoft Store Policies version 7.20** (published
> 2026-09-15, effective 2026-10-22) and the MSIX packaging docs. Policy version moves —
> re-check the version before acting on any line here.
>
> **This file is the "why".** The executable checklist lives in
> [`task-48-distribution-msix.md`](task-48-distribution-msix.md). No code changed as a
> result of this research; it only decides what task-48 will eventually do.
---
## 1. Bottom line
**MSIX packaging is viable.** One real blocker, and it is a code change we should want
anyway (§4). Three technical blockers that normally kill a Store submission — drivers,
per-user services, elevation — we **do not trip** (§7).
**The decision is NOT made.** The route is open; this file records the options and their
costs so the decision can be made from a clear page. `Distribution.md` holds the older
plan and contains **one factually dead claim** — see §2.
---
## 2. ⚠️ The one line in `Distribution.md` that is now wrong
`Distribution.md:318` currently says:
> Buy an EV (Extended Validation) code-signing certificate from DigiCert or Sectigo
> ($200-400/year). EV certificates get instant SmartScreen reputation; DV/OV certs
> trigger SmartScreen warnings for months.
**The EV half of that is false.** Microsoft removed the SmartScreen instant-bypass for EV
certificates in **March 2024**. An EV-signed file now accumulates reputation exactly the
way an OV-signed file does. Paying $400+/yr buys precisely what paying $150 buys.
`schema.md` rule 4: stale facts get FIXED, not appended. Corrected in `Distribution.md`
(2026-09-27).
Also worth knowing before budgeting:
- **Cert validity is capped at 460 days** (CA/B Forum CSC-31, effective 2026-03-01). This
is a recurring annual line item, not a one-time purchase.
- **Private keys must live on an HSM or hardware token** (CA/B rule since 2023-05). Budget
for the token or a cloud HSM.
- **Signing does not clear the warning on day one.** A valid cert stops the *malware*
warning immediately; the "unrecognized publisher" warning clears as SmartScreen accrues
reputation from real download volume. Expect a ramp on every route.
- **Third-party AV vendors layer their own reputation blocking on top of SmartScreen**, so
signed-but-new can still warn on consumer machines. The Store route is the only one that
is warning-free, because Microsoft holds the reputation.
---
## 3. Signing economics
| Route | Cert cost | SmartScreen | Install UX | Notes |
|---|---|---|---|---|
| **Microsoft Store, MSIX package** | **$0** — Microsoft re-signs | No warning | Normal | Store review, public listing, updates handled by Store |
| **Microsoft Store, EXE/MSI (10.2.9)** | **$150–300/yr — your own cert** | Warning ramp | **Silent install only** | Store does NOT re-sign; you maintain a versioned URL |
| **Direct + OV cert** | $150–300/yr | Warning ramp | Yours | Worldwide availability |
| **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120/yr | Warning ramp | Yours | **Individuals: USA/Canada only.** Identity validation required |
| **Direct + EV cert** | $400+/yr | Same as OV | Yours | **Do not buy.** No benefit since March 2024 |
| **Unsigned direct** | $0 | Strong block | Yours | Unusable for public download |
### Four real combinations
| # | Distribute | Verify payment | Cert | Revenue cut |
|---|---|---|---|---|
| **A** | Store | Store IAP | $0 | yes |
| **B** | Store | **Polar** | $0 | no |
| **C** | Polar file hosting | Polar | $150–300/yr | no |
| **D** | Store EXE (10.2.9) | Polar | $150–300/yr | no |
**A** is the only one where Polar becomes unnecessary — the Store handles payment,
entitlement, and refunds, and you would delete `PolarLicenseService` and the
customer-portal plumbing. That is a *distribution* choice that happens to subsume
licensing.
**B** is the compromise worth serious consideration: the Store solves the SmartScreen
problem and the annual cert bill while Polar stays the licensing system and we keep 100%
of revenue. Cost is two systems to maintain.
**C** is the status quo already sketched in `Distribution.md:14` / `:296` — Polar hosts the
signed exe (10GB, signed personal download URLs, SHA-256 included). **The Store is not
needed for delivery at all.** The Store's only unique value is *free Microsoft signing with
no SmartScreen warning*, and its cost is MSIX packaging work, Store review, and a public
listing.
**D is dominated** — strictly worse than both A and C: cert required anyway, silent install
required, and we maintain the versioned download URL. Documented in one line so it is
never re-litigated.
---
## 4. ⛔ The one real blocker: 10.2.2 (dynamic code inclusion)
**Policy 10.2.2:**
> Your product must not attempt to fundamentally change or extend its described
> functionality or introduce features or functionality that is in violation of Store
> Policies **through any form of dynamic inclusion of code**. Your product should not, for
> example, **download a remote script and subsequently execute that script** in a manner
> that is not consistent with the described functionality.
**What we do today:** `Services/Encoder/FfmpegLocator.cs:37-38` pins a GitHub release URL,
and `LocateAsync` downloads the zip (line 69), extracts it (line 73), and the encoder then
executes that binary. On a cold cache the app **downloads an unsigned executable from the
internet and runs it.**
That is the pattern 10.2.2 names. The word is "script," so a narrow reviewer might quibble
about a PE binary — but 10.2.3 reinforces it, and 10.2.9 separately requires all PE files
to chain to the Microsoft Trusted Root Program, which a downloaded BtbN build does not.
**The fix:** bundle `ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family **inside the
package** instead of downloading. No dynamic code inclusion, so 10.2.2 is satisfied.
**This is a fix we want regardless of route.** `FfmpegLocator.cs:30-35` records that the
previous daily pin aged out of GitHub retention and **404'd on the first real recording
attempt (2026-09-01)**. Bundling kills that entire class of cold-start failure permanently,
and it also removes the startup network dependency.
The LGPL-shared licensing reasoning in `FfmpegLocator` is unaffected — dynamic linking was
the deliberate choice for LGPL §6 compliance, not for download avoidance. Confirm
`THIRD-PARTY-NOTICES.txt` (already shipped) covers the bundled build.
**What I got wrong initially:** I believed MSIX might forbid *launching* a bundled
executable. It does not — see §5.
---
## 5. MSIX technical facts (full trust vs AppContainer)
MSIX packages run at one of two trust levels, chosen by the manifest:
| Trust level | Behavior |
|---|---|
| **`mediumIL` / full trust** | **Same permissions as a standard desktop app.** Package identity + some file/registry virtualization. **Can spawn child processes.** ← this is what a WPF broadcast tool wants |
| `appContainer` | Strictly isolated. Process and children see only explicitly granted resources. File/registry virtualized. |
**A packaged WPF app declaring `runFullTrust` runs at medium integrity — it is not an
AppContainer.** Bundling and launching ffmpeg from inside the package is therefore fine.
This is the "Desktop Bridge" / `packagedClassicApp` model.
**Critical file-write consequence:** "Writes to user profile locations (such as AppData) are
redirected to per-package locations for AppContainer apps, or **pass through for full-trust
apps**." The package install folder itself is read-only.
**Manifest this app needs:**
- `rescap:Capability Name="runFullTrust"` — medium integrity
- `webcam` and `microphone` capabilities
- `uap10:TrustLevel="mediumIL"`
- `uap10:RuntimeBehavior="packagedClassicApp"`
### The full-trust recording invariant (durable architectural fact)
`ViewModels/MainViewModel.Recording.cs` writes recordings to
`%USERPROFILE%\Downloads` or `SpecialFolder.MyVideos` (`DefaultRecordFolder()`, line
173–179), with `Microsoft.Win32.OpenFolderDialog` for overrides (`ChooseRecordFolder()`,
line 151). The whole temp-write-then-move lifecycle stays inside one directory
(line 131–132).
**This works only because we are full trust.** At `mediumIL` those writes pass through
unvirtualized and `OpenFolderDialog` is an ordinary Win32 folder browser with no capability
token involved. **No `broadFileSystemAccess` is needed.**
⚠️ **If anyone ever flips the manifest to `appContainer`, recording silently breaks** —
`Directory.CreateDirectory` and `File.Move` start resolving to a per-package virtualized
location. A code comment belongs at `DefaultRecordFolder()` **when the manifest lands**, not
before (a comment describing a manifest that does not exist is worse than no comment).
---
## 6. Store policy verdicts — what binds, what doesn't
Policy 7.20. "Binds" = must be satisfied to submit.
### ⛔ Binds — blockers or real work
| Policy | Requirement | Our status |
|---|---|---|
| **10.2.2** | No dynamic code inclusion (download-and-execute) | ❌ **VIOLATED** — see §4. Fix: bundle ffmpeg |
| **10.3.1** | Login required → supply a **working demo account** in certification notes | ☐ TODO — we require YouTube sign-in |
| **10.5.1** | **Privacy policy mandatory**; URL in Partner Center. Desktop Bridge + Win32 **always** need one. Must state what PI is accessed/collected/transmitted, how used/stored/secured, types of parties disclosed to, controls the user has, compliance with law, and be kept current | ☐ TODO — `llamachile.shop` can host it |
| **10.5.4** | Collect/store/transmit PI securely with modern cryptography | ✅ DPAPI for tokens, HTTPS to YouTube |
| **10.5.5** | No highly sensitive PI (health/**financial**) unless tied to functionality + express consent | ⚠️ **SuperChat/reward path touches financial data** — keep display-text-only processing, never log payloads |
| **10.6** | Declared capabilities must relate to the product's functions; **must not circumvent OS checks** | ✅ camera/mic for a broadcast tool is self-evident; must use standard MF/DShow/WASAPI paths |
| **10.7** | Localize for every declared language; declare **English only** | ☐ Keep the manifest to one language |
| **10.1.1** | Title must be unique, **no marketing or descriptive text, no extraneous keywords** | Store name is exactly `LlamaCasty` |
| **10.1.3** | Search terms: max 7, no pricing terms, **no other product titles** unless you published them | ☐ Cannot list `OBS` or `StreamYard` as search terms |
| **10.1.4** | Distinct informative metadata + **active Store presence** | ☐ Listing must not be a stub |
| **10.2.7** | Must enable clean uninstall | ☐ MSIX gives this free |
| **10.4.2** | Start promptly, stay responsive, shut down gracefully, survive native API exceptions | ✅ `AppLog` + dispatcher handlers exist |
| **11.11.1** | **IARC age-rating questionnaire** required | ☐ General-audience streaming tool, 12+ territory |
| **11.11.2** | Keep the rating accurate as features change | ☐ Re-visit if audience-facing features shift |
| **11.12** | **User Generated Content** — see §9 | ⚠️ Judgment call, documented not built |
| **10.8.1** | Non-game PC products **may** use a secure third-party purchase API **or** Store IAP | ✅ **Polar explicitly permitted.** Must tick the third-party purchase box in Partner Center |
| **10.8.2** | Third-party purchase: identify provider, authenticate user, obtain confirmation, PCI DSS, note in Partner Center | ☐ Covered by Polar; tick the box |
| **10.8.3** | **Individual** accounts cannot require financial info for primary functionality; requiring it forces a **Company** account | ⚠️ **Open question** — see §11 |
| **10.14** | Individual accounts usually fine for a solo dev; Company required if the product needs financial info for primary functionality, or a consumer would read the name as a business | ⚠️ Same question as 10.8.3 |
| **10.8.7** | Pricing must follow the FTC Guides Against Deceptive Pricing and must not be irrationally high vs. features | ☐ A pricing decision, not a code task |
| **10.5.8** | Must **follow** applicable safety/privacy laws re children | ☐ Follow-the-law obligation — see §8 |
| **10.1.5** | May enable acquisition of your other Store products or add-ons | N/A |
### ✅ Does NOT bind — recorded so it is never re-raised
| Policy | Why it doesn't apply |
|---|---|
| **10.10** (Advertising) | **Ads are injected server-side by YouTube after the RTMP handoff.** LlamaCasty displays no ads, renders no ad SDK, and controls no ad decision. Every 10.10 clause is conditioned on ads *your product displays*. Not applicable. |
| **10.2.5** (Store-only install) | 7.20 applies "installed and updated only through the Store" to **game products and Xbox console products**. A non-game PC app is not locked to MSIX. *(The 2018 v7.7 wording said "all of your apps" — that language has been narrowed.)* |
| **10.2.3** (secondary software) | ffmpeg **enhances** the product's functionality, and once bundled it is not "offered to install" as separate software |
| **10.2.8** (Windows settings) | `RegisterHotKey` and standard DShow/MF paths are supported APIs. No accessibility-API or undocumented-API tricks |
| **10.10.1 / 10.10.2** | Follows from 10.10 — no ads displayed |
| **11.13** (Storefront) | Not a storefront |
| **11.14** (Gambling) | Not applicable |
| **11.15** (Child Safety) | Prohibits products enabling exploitation/grooming/sexualization/sextortion/trafficking, and adult-themed products targeting children. A broadcast tool is not in this class. Nothing LlamaCasty does creates, uploads, or distributes child-exploiting content |
| **10.5.2** | We do not publish a customer's PI to an outside service beyond the stream they themselves broadcast |
| **10.5.7** (location) | No device location access |
| **10.2.9** | Only if we chose the Store EXE route. It would bind — see §3, route D is dominated |
### Certification mechanics
- Technical compliance is tested by the **Windows App Certification Kit** before submission.
- Certification takes **up to 3 business days**; the listing appears ~15 min after publish.
- MSIX package requirements: SHA2-256 block map, `StoreManifest`, 25 GB max.
- Partner Center account: individual vs company — see §11.
---
## 7. MSIX disqualifiers we do NOT trip
Verified by grep across the codebase 2026-09-27. The MSIX prep docs list four hard
disqualifiers; **all four are clear**:
| Disqualifier | Our status |
|---|---|
| "Your application requires a Windows driver. MSIX doesn't support Windows drivers." | ✅ We **consume** DShow filters, we don't install a driver. (A creator's third-party Virtual Capture Device is their hardware, not something we ship.) |
| "Your application requires a user Windows service. MSIX doesn't support per-user Windows services." | ✅ No service installation anywhere |
| Apps requiring **elevation** for any functionality are not accepted | ✅ No `requireAdministrator` / `highestAvailable` / UAC manifest |
| "Your app's modules are loaded in-process to processes that are not in your Windows app package" (shell extensions, etc.) | ✅ No shell extensions, no in-process module loading, no jump lists |
Also verified: there is **no `.manifest` file in the repo at all** today, so packaging is
purely additive — a new `Package.appxmanifest` plus a packaging project. The existing
unpackaged exe keeps working.
### Other store rules worth knowing while listing
- **10.1.4** requires an active Store presence — the listing cannot be a placeholder.
- **10.7** — declare English only, or localize the description into every declared language.
- **10.9** — the app must stay functional when notifications are disabled. Our toast stack
must not be load-bearing.
---
## 8. Minors, COPPA, and the age question
### The operator is 13+ by construction
YouTube Terms of Service:
- **"You must be at least 13 years old to use the Service"**
- 13–17 permitted **with parent/guardian permission**
- **Under 13 cannot create an account** — exceptions are YouTube Kids and supervised
accounts via Family Link or Google Workspace for Education
- Google Accounts: 13 is the minimum to self-manage
- **The floor varies by region** — at least one YouTube ToS variant states 14, tracking EU
digital-consent ages. Treat it as 13–14, not a fixed 13.
**Why this does real work:** LlamaCasty *requires* a YouTube account to operate, because
streaming requires one. So the person running the broadcast is 13+ by construction. A
COPPA theory premised on "a service collecting personal information from children under 13"
has no purchase against the operator — and that is true **by construction, not by promise**.
That is the cleanest sentence available for a certification note.
### The residual exposure
The **viewers** may still be under 13 on supervised/family-linked accounts, so the chat
ingestion point does not fully disappear. It weakens substantially, because:
- Live chat is restricted/disabled on supervised YouTube kid accounts
- YouTube moderates the messages before we ever see them (§9)
- Our mitigation: **retain nothing** — parse, display, discard (see `task-49`)
### COPPA is very unlikely to bind
COPPA applies to child-directed services and to operators collecting PI from children under
13. The three-factor test (subject matter, audience, evidence of child focus) is nowhere
near met by a broadcast tool aimed at a gaming audience. **10.5.8 is a follow-the-law
obligation, not a be-child-safe obligation** — it says "follow all applicable laws," it does
not impose COPPA duties on us directly.
**But note the enforcement trend** (FTC, September 2025 Apitor settlement): operators are
held responsible for **what third-party components collect on their behalf**, and the FTC
expects a real inquiry into third-party collection practices. That case was about an SDK;
the principle is what matters for a chat pipe. This is a documentation obligation, and the
answer is "we integrate no third-party analytics or ad SDKs, and we persist no chat or
reward payloads."
---
## 9. 11.12 User Generated Content — the judgment call
11.12 triggers on "content that users contribute to a product and which can be viewed or
accessed by other users **in an online state**." **Chat is literally that.** If a reviewer
classifies the chat surface as UGC, the requirements are:
1. Published **terms of service, code of conduct, and/or content guidelines** for UGC,
accessible **in-product and on your website**
2. A **means for users to report** inappropriate/illegal/harmful UGC to the developer for
review, and/or proactive detection
3. Respect **user safety, parental, and content privilege settings**, and handle restricted
access gracefully
4. **Remove or disable UGC when requested by Microsoft**
5. Disclose that content is **not created by the developer/publisher** where appropriate
### The argument: YouTube discharges the moderation obligation upstream
| 11.12 asks for | How YouTube already covers it |
|---|---|
| Published ToS / CoC / content guidelines | Community Guidelines, linkable from our listing |
| A reporting mechanism | Creator-side chat reporting; YouTube enforces |
| Respect parental / privilege settings | YouTube's restricted + supervised accounts |
| Remove/disable on request | YouTube removes; we never hosted the original |
| Disclose third-party UGC origin | Visibly YouTube chat; YouTube is platform of record |
**And the enforcement is demonstrably real.** YouTube's transparency report for
**Jan–Mar 2026**: approximately **1.6 billion comments removed in a single quarter**, with
**child safety the #2 removal reason at 124,624,293** (hateful/abusive 15,977,127;
harassment/cyberbullying 71,326,861; spam/scams 1.31 B).
**The certification-note sentence:**
> UGC is mirrored from YouTube, which moderates it at industrial scale upstream. We render
> transiently, persist nothing, and provide no user-to-user communication surface.
**The "persist nothing" half is ours to keep** — and it is the same property
`task-49` (profanity filter) needs. Convenient, not coincidental.
**Our read:** 11.12 targets products hosting a UGC *community*. A chat *mirror* rendered
from YouTube — where YouTube is the platform of record and already supplies reporting,
age-gating, and parental controls — is arguably not "our" UGC. **This is a judgment call,
and we resolve it by documenting the position rather than building features on a guess.**
State the reasoning in the certification notes; Microsoft reads them.
### The documentation we owe regardless
| Artifact | Requirement |
|---|---|
| **Privacy policy** on `llamachile.shop` | 10.5.1. Must state: camera/mic access is user-directed and OS-gated; **no retention** of chat or reward payloads; no viewer data collected; **ffmpeg is bundled, nothing is fetched at runtime** |
| **Code of conduct + content guidelines** | 11.12 / 11.15. General audience; explicitly not directed at under-13s |
| **IARC age-rating questionnaire** | 10.11.1 |
| **Certification notes** | the 11.12 reading, the YouTube age-gate argument, the Polar third-party purchase box, the demo account |
---
## 10. Camera, microphone, and "only when the user says so"
**We do not write that certification. Windows does it — and the Store forbids us from
bypassing it.** Three layers:
1. **Policy 10.6** — "You must not circumvent operating system checks for capability
usage." Route through the standard OS consent path; do not roll our own.
2. **Windows privacy settings** — Settings → Privacy & security → Camera / Microphone.
Since **Windows 10 1903**, *"Let desktop apps access your camera/microphone"* is the
toggle that governs a Win32 broadcast app. Turn it off and the app loses access. **That
is the "explicit user direction" mechanism, enforced by the OS, not by our EULA.**
3. **Hardware/OS indicators** — camera light (or a notification when there is no light) and
a microphone icon in the taskbar tray. Automatic. The OS is the disclosure mechanism, so
we get neither credit nor blame from it.
### ⚠️ The open question — measure, don't assume
Microsoft's own support page carries this caveat:
> Desktop apps may not always appear in the list of apps available to the Camera and
> Microphone settings pages or might still be able to access your camera or microphone even
> when these settings are turned off.
**Untested: does the Win11 desktop-app camera toggle reliably gate a DShow webcam and a
capture card?**
- If it **does** gate them, we can make privacy-forward claims after verifying.
- If it **does not**, that is **not** a Store certification failure — we declared `webcam`
honestly and use supported APIs — but it is a real user-trust issue, and we must not
write privacy-forward marketing copy on the assumption that it works.
**Tracked as an open item in `TASKS.md`. A guard note in `MARCOM.md` blocks privacy-forward
copy until it passes.**
---
## 11. Open questions
1. **Individual or Company Partner Center account?** (10.8.3 / 10.14.) Individual accounts
are "usually appropriate for a single developer working on their own," and a Company
account is required if the product requires financial account information **for primary
functionality**. Polar collects payment externally and we never ask the user for card
details, so we look like a clean individual case — but it is a question for Partner
Center, and getting it wrong means re-enrolling.
2. **Does the desktop-app camera toggle gate DShow?** (§10) — gates the `MARCOM.md`
privacy claim.
3. **MSIX write behavior under a real package identity** — full trust passes writes through
per the docs, but worth confirming on a real packaged build before trusting it with
recordings.
4. **Store revenue-share terms** — do not plan on any specific percentage. The terms have
moved more than once and small indie apps sometimes qualify for better rates. Verify
current terms and conditions before pricing against them.
5. **`liveBroadcasts.insert` requires `status.selfDeclaredMadeForKids`** (COPPA) — already
known, see [`research-youtube-api.md`](research-youtube-api.md) item 1. Confirm the
dialog wording is right for a tool that is never child-directed.
---
## 12. Profanity filter — design constraints (feeds `task-49`)
YouTube's Community Guidelines carry a specific **Vulgar language policy** under
"Sensitive content," alongside nudity/sexual content, child safety, and self-harm. So
profanity is a named policy surface, not an edge case.
Three external reasons it earns a slot, plus the real one:
1. YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has
something to attach to
2. Competitive parity — overlay tools in this lane generally offer it
3. It is a reasonable reading of 11.12's proactive-detection language
4. **The real reason: we control what gets projected on screen.** That is the creator's
call, not YouTube's.
**Four constraints, because they protect the privacy story rather than complicate it:**
1. **Local and on-device only.** No server, no telemetry of message content, nothing leaves
the machine.
2. **Non-persistent.** Filter at ingest, discard the raw text, keep only the display
string. Same property that makes the 11.12 answer in §9 strong.
3. **User-supplied word list**, opt-in, stored locally. **Not a hardcoded slur list baked
into the binary** — that is an unpleasant artifact, generates false positives across
dialects and languages, and becomes our problem the moment someone extracts it. A
user-curated list is also simply more useful: streamers know their community's
euphemisms and we do not.
4. **Match display text only.** Never the SuperChat amount or any reward-event field —
those are financial data under 10.5.5, and matching on them would be a real mistake.
The existing alert box already has a ticker and three display methods
(`Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Chat.cs`), so there is probably a
**single insertion point** rather than scattered call sites. Verify before building.
+187
View File
@@ -0,0 +1,187 @@
# TASK 48 — Distribution & packaging: route decision, MSIX, signing
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
> **Research: [`TASKS/research-store-certification.md`](research-store-certification.md)** — the "why".
> Carved out of TASK 36 item 6 (release engineering) on 2026-09-27 so distribution has one
> owner instead of three scattered mentions.
**Status:** ☐ Queued — **⏳ blocked on the creator's route decision** (item 0)
**Goal:** get LlamaCasty in front of a user without a SmartScreen scarewall, ideally without
an annual certificate bill, and without breaking recording, capture, or audio.
⚠️ **Nothing in this task is decided yet.** The research is done and MSIX is the
front-runner, but the route is the creator's call. Items 2–8 describe the MSIX path
*conditional on choosing it* — if the answer is Polar-hosted + a cert, most of them evaporate
and only items 0, 1, and 9 remain.
---
## 0. ⛔ THE DECISION — creator, not AI
Pick one:
| # | Route | Cert/yr | SmartScreen | Extra work |
|---|---|---|---|---|
| **A** | Store MSIX + Store IAP | **$0** | none | MSIX packaging; Store review; **Polar deleted**; revenue cut |
| **B** | Store MSIX + **Polar** | **$0** | none | MSIX packaging; Store review; two systems to maintain |
| **C** | **Polar file hosting** + Polar + own cert | $120–300 | warning ramp | none beyond buying the cert |
| **D** | Store EXE (10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway + silent install + maintain our own URL |
**C is the status quo already sketched in `Distribution.md:14`/`:296`** — Polar hosts the
signed exe (10GB, signed personal URLs, SHA-256). The Store is not needed for delivery at
all; its only unique value is *free Microsoft signing with no warning*.
Cost table, full detail, and the dead-EV correction: `research-store-certification.md` §2–3.
---
## 1. ⛔ Bundle ffmpeg instead of downloading it — **do this on EVERY route**
**Not conditional on the Store. This is worth doing regardless.**
`Services/Encoder/FfmpegLocator.cs:37-38` pins a GitHub release URL; `LocateAsync`
downloads (line 69), extracts (line 73), and the encoder executes the result. On a cold
cache the app downloads an unsigned executable from the internet and runs it.
- **Store blocker:** policy 10.2.2 forbids dynamic code inclusion (download-and-execute).
- **Route-independent bug class:** `FfmpegLocator.cs:30-35` records that the previous
daily pin aged out of GitHub retention and **404'd on the first real recording attempt
(2026-09-01)**. Bundling kills this permanently and removes the startup network
dependency.
**Do:** ship `ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family inside the package;
`FfmpegLocator` resolves PATH → package-local → cache, and never downloads.
`IFfmpegLocator`'s contract, the `ExtractBinaries` staging discipline, and the existing
`FfmpegLocatorTests` cold/cache/PATH coverage all still apply.
**Licensing:** the LGPL-shared build was chosen for LGPL §6 compliance (dynamic linking =
"license text + source offer"). That reasoning is **unaffected**. Confirm
`THIRD-PARTY-NOTICES.txt` covers the bundled build.
**Record the immutable tag** in `TASKS.md` per the licensing rule, and note the URL is now
a provenance record rather than a runtime fetch.
---
## 2. ☐ `Package.appxmanifest` — full trust, camera, microphone
Only if the Store is chosen. There is currently **no `.manifest` file in the repo at all**,
so this is purely additive.
- `rescap:Capability Name="runFullTrust"` — medium integrity, not AppContainer
- `webcam` and `microphone` capabilities
- `uap10:TrustLevel="mediumIL"`, `uap10:RuntimeBehavior="packagedClassicApp"`
- `uap10:RuntimeBehavior="packagedClassicApp"` is what allows launching package
executables as child processes
- Declare **English only** (10.7 — otherwise the description must be localized into every
declared language)
- Block map SHA2-256, `StoreManifest`, under the 25 GB cap
**Do NOT use `appContainer`** — see the invariant in item 6.
## 3. ☐ Remove Velopack — 3 edit sites
Only if the Store is chosen (the Store handles updates). Trivially removable; the code was
written defensively for exactly this.
- `ytLive.csproj:92` — `<PackageReference Include="Velopack" Version="1.2.0" />`
- `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"
**Retires:** the pending "Velopack update URL" item (`TASKS.md` open items,
`task-10-monetization.md:43`) and the self-hosted DigitalOcean droplet.
## 4. ☐ Windows App Certification Kit
Run before submission. Technical compliance is tested by WACK; it is not optional. Known
relevant check: the app must start promptly, stay responsive, and shut down gracefully
(10.4.2).
## 5. ☐ Certification notes + the three documentation artifacts
**In Partner Center (submission):**
- ☐ **Working YouTube demo account** (10.3.1) — required, we cannot stream without sign-in
- ☐ Tick the **secure third-party purchase API** box — Polar (10.8.1 / 10.8.2)
- ☐ **The 11.12 UGC position**, stated in full — "mirrored from YouTube, which moderates it
at industrial scale upstream; we render transiently, persist nothing, and provide no
user-to-user communication surface." Full reasoning and the Q1-2026 enforcement numbers
are in `research-store-certification.md` §9
- ☐ The **YouTube age-gate argument** — operator is 13+ by construction (§8)
- ☐ Note that the product is **general audience, not directed at under-13s**
**On `llamachile.shop`:**
- ☐ **Privacy policy** (10.5.1 — mandatory for Win32/Desktop Bridge). Must state: camera/mic
access is user-directed and OS-gated; **no retention** of chat or reward payloads; no
viewer data collected; **ffmpeg is bundled and nothing is fetched at runtime**
- ☐ **Code of conduct + content guidelines** (11.12 / 11.15)
- ☐ Confirm `THIRD-PARTY-NOTICES.txt` covers the bundled LGPL ffmpeg build
**In Partner Center (listing):**
- ☐ **IARC age-rating questionnaire** (10.11.1) — general audience, 12+ territory
- ☐ Title is exactly **`LlamaCasty`** — 10.1.1 forbids marketing text or extraneous keywords
- ☐ Search terms: max 7, no pricing terms, and **no other product titles** (10.1.3 — cannot
list `OBS` or `StreamYard`)
- ☐ Real listing content — 10.1.4 requires an active, substantive presence
- ☐ Review the **Individual vs Company** account question before enrolling (see
`research-store-certification.md` §11 item 1) — getting this wrong means re-enrolling
## 6. ☐ The full-trust recording invariant — **comment lands WITH this work**
`ViewModels/MainViewModel.Recording.cs:173-179` (`DefaultRecordFolder()`) writes to
`%USERPROFILE%\Downloads` or `SpecialFolder.MyVideos`; `ChooseRecordFolder()` (line 151)
uses `Microsoft.Win32.OpenFolderDialog`; the temp-write-then-move lifecycle stays in one
directory (line 131-132).
**This works only because the package is full trust.** At `mediumIL`, user-profile writes
pass through unvirtualized and `OpenFolderDialog` is an ordinary Win32 browser with no
capability token. No `broadFileSystemAccess` needed.
⚠️ **If anyone flips the manifest to `appContainer`, recording silently breaks** —
`Directory.CreateDirectory` and `File.Move` start resolving to a per-package virtualized
location and output vanishes.
**Add a comment at `DefaultRecordFolder()` in this task, not before.** A comment describing
a manifest that does not exist is worse than no comment.
**Also verify on a real packaged build** before trusting it with recordings — the docs say
full trust passes writes through, but that is worth confirming with an actual package
identity rather than an unpackaged run.
## 7. ☐ Confirm the disqualifiers stay clear after packaging
All four are currently clear (verified by grep 2026-09-27) — re-verify once the packaging
project exists:
- No Windows driver installed (we consume DShow filters, we do not install one)
- No per-user Windows service
- No elevation / `requireAdministrator` / UAC manifest
- No shell extension, no in-process module loading by outside processes, no jump list
## 8. ☐ Pre-submission gates
- ☐ 0 warnings, full suite green
- ☐ **Vertical-recording path verified end-to-end** (compositor tier is proven by
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
been exercised — `Services/Pump/FramePump.cs:645` flags off-size tiers as a known
follow-up)
- ☐ **The real-output confirmation** the creator has been doing manually
- ☐ Camera/mic/wasapi capture verified **from the packaged build**, not just unpackaged
- ☐ Clean uninstall verified (10.2.7)
- ☐ Notification-disabled path leaves the app functional (10.9)
---
## Not in this task (deliberately)
- **Velopack hardening / obfuscation / assembly splitting** — stays a GA-time decision per
TASK 36 item 6's agreed ceiling. Compile flags max at `HARDENED` + `MOCK_REWARDS`,
additive-only.
- **EULA draft/review and the THIRD-PARTY-NOTICES gate** — stay in TASK 36 item 6.
- **Vertical-canvas feature work** — the feature exists; only the *record-path test* above
is missing.
- **macOS / Avalonia port** — deferred; would be a second app, not a port.
+94
View File
@@ -0,0 +1,94 @@
# TASK 49 — Chat profanity filter (local, opt-in, non-persistent)
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
> **Research: [`TASKS/research-store-certification.md`](research-store-certification.md) §12.**
> Carved out 2026-09-27; shares a scope lock with nothing else.
**Status:** ☐ Queued — not blocked (does not depend on the route decision)
**Goal:** let the creator decide what reaches the projected chat box, without the app ever
becoming a moderator that stores what people said.
**Size:** S. Local string work, one insertion point, tests.
---
## Why
YouTube's Community Guidelines carry a named **Vulgar language policy** under "Sensitive
content", alongside nudity, child safety, and self-harm — so profanity is a policy surface,
not an edge case. Three external reasons it earns a slot, plus the real one:
1. YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has
something to attach to
2. Competitive parity — overlay tools in this lane generally offer it
3. It is a reasonable reading of 11.12's proactive-detection language
4. **The real reason: we control what gets projected on screen.** That is the creator's
call, not YouTube's.
---
## 1. ⛔ Four constraints — non-negotiable
They exist to **protect the privacy story**, not to complicate the build:
1. **Local and on-device only.** No server, no telemetry of message content, nothing leaves
the machine. A hosted profanity API would invert the entire 11.12/10.5.1 argument in
`research-store-certification.md` §9 — do not add one.
2. **Non-persistent.** Filter at ingest, discard the raw text, keep only the display
string. No message history, no file, no crash dump of chat payloads. This is the same
property that makes the 11.12 certification answer strong.
3. **User-supplied word list, opt-in, stored locally.** ⛔ **Not a hardcoded slur list
baked into the binary.** That is an unpleasant artifact, generates false positives
across dialects and languages, and becomes our problem the moment someone extracts it.
A user-curated list is also simply more useful — streamers know their community's
euphemisms and we do not.
4. **Match display text only.** ⛔ Never the SuperChat amount or any reward-event field.
Those are financial data under 10.5.5, and matching on them would be a real mistake.
## 2. ☐ Verify the insertion point first
The existing alert box already has a ticker and three display methods
(`Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Chat.cs`), so there is probably a
**single insertion point** rather than scattered call sites.
**Do not assume.** `rg` for the chat-message ingest path and confirm the count. If it is
more than one site, say so in `HANDOFF.md` before building.
## 3. ☐ Implementation
- ☐ Settings toggle: **off by default** (opt-in, per constraint 3)
- ☐ Editable word list in the settings dialog, stored locally via the existing
`LayoutStore.Settings` pattern — reuse it, do not invent a second store
- ☐ Case-insensitive, **whole-word** matching with sensible boundary handling
- ☐ Smallest sensible replacement (`*` or configurable) — do not build a masking engine
- ☐ Filtered messages are **displayed** with the replacement, not dropped, so the chat does
not develop visible holes. (Dropping is a separate opt-in if we ever want it.)
- ☐ Nothing logged. The existing `AppLog` checkpoints must not capture message text
- ☐ Correctness over performance: a few hundred comparisons per message is nothing
- ☐ Non-ASCII and unicode case handling — a naive `ToLowerInvariant` will miss things
- ☐ Ticker layout must survive longer replacement strings
## 4. ☐ Tests
Follow the existing test conventions (`ytLive.Tests/`, `RuntimeSessionStateTests` etc.):
- ☐ Off by default
- ☐ List matches case-insensitively
- ☐ Whole-word only — "class" must not be caught by "ass"
- ☐ Replacement renders, message not dropped
- ☐ **Amount/reward fields are never matched or masked** (constraint 4 — assert explicitly)
- ☐ Empty list behaves as "no filtering"
- ☐ Nothing persists to disk beyond the user's own list
- ☐ Unicode case folding
---
## Related (not duplicated here)
- [`task-48-distribution-msix.md`](task-48-distribution-msix.md) item 5 — the privacy
policy must state "no retention of chat or reward payloads". This task is what makes that
statement true.
- `research-store-certification.md` §9 — the 11.12 argument depends on the
persist-nothing property in constraint 2. **Do not let a future feature (chat history,
moderation logs, analytics) quietly break that assumption.**
+108
View File
@@ -634,6 +634,22 @@ const). Constructor-injected search dirs / tools dir / downloader (`Func<string,
Task<byte[]>>`) keep it hermetic: tests fake the network with a real in-memory zip. Constructed in the
encoder step (not yet — this PR ships the seam + impl + tests only).
**⛔ The pinned-download design is now formally a defect, not a cold-start edge case (2026-09-27).**
It **failed in production on 2026-09-01** — the previous daily pin aged out of BtbN's 14-day
retention and the download 404'd on the creator's first real recording attempt. Two facts make
the "pin is a const, just bump it" assumption wrong:
- **Microsoft Store policy 10.2.2 forbids dynamic code inclusion.** Downloading an executable
from the internet and running it is the exact pattern the policy names. An MSIX submission
is rejected over it.
- Bumping the pin is whack-a-mole against a retention window we do not control.
**Fix (TASK 48 item 1, recommended on EVERY route — it is not Store-conditional):** bundle
`ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family **inside the package**; `FfmpegLocator`
resolves PATH → package-local → cache and never downloads. This also deletes the startup
network dependency entirely. The LGPL-**shared** build choice is **unaffected** — dynamic
linking was chosen for LGPL §6 compliance ("license text + source offer"), not to enable
downloading. Analysis: `TASKS/research-store-certification.md` §4.
### Live encoder + RTMP push (TASK 4 ship step 3 — shipped 2026-08-12, plan in TASKS.md)
The encoder is a **thin orchestrator over `ffmpeg.exe`** — no H.264/AAC code in the app. It spawns the
@@ -1775,3 +1791,95 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
dedicated 8s stereo-48k ring (`AlertBufferSeconds`), drained in `FillAndMix` before the
limiter and added at **unity — never ducked** (creator ruling: "don't lower my game
audio for a tip"); `StartLive` clears it. Per-alert loudness = the Volume slider.
---
## Windows packaging & distribution (2026-09-27 — research settled, route NOT decided)
Full analysis: `TASKS/research-store-certification.md` (Store Policies **7.20**, effective
2026-10-22 — re-check the version before acting). Executable checklist:
`TASKS/task-48-distribution-msix.md`. **The route is the creator's decision** (Store MSIX +
Store IAP / Store MSIX + Polar / Polar-hosted + own cert / dominated Store-EXE). Do not build
packaging work before that call, and do not re-derive the options — they are tabulated in the
research file §3.
### ⛔ Durable invariant: full trust, or recording silently breaks
MSIX packages run at one of two trust levels. A packaged WPF app declaring
`rescap:Capability Name="runFullTrust"` runs at **mediumIL — the same permissions as a
standard desktop app, NOT AppContainer** — and may spawn child processes, which is why
bundling ffmpeg is legal. Writes to user-profile locations (AppData, Downloads, MyVideos)
**pass through unvirtualized at mediumIL**; the package install folder is read-only.
**`ViewModels/MainViewModel.Recording.cs` writes recordings to
`%USERPROFILE%\Downloads` / `SpecialFolder.MyVideos` (`DefaultRecordFolder()`,
`ChooseRecordFolder()` via `Microsoft.Win32.OpenFolderDialog`), and the whole
temp-write-then-move lifecycle stays in one directory.** That works *only* because we are full
trust. No `broadFileSystemAccess` capability is needed, and none should be added.
⚠️ **If anyone flips the manifest to `appContainer`, `Directory.CreateDirectory` and
`File.Move` start resolving to a per-package virtualized location and output vanishes with no
error.** A comment at `DefaultRecordFolder()` is owed **when the manifest lands** (TASK 48
item 6) — not before; a comment describing a manifest that doesn't exist is worse than none.
### Signing — do not buy EV
Microsoft **removed the SmartScreen instant-bypass for EV certificates in March 2024**. An
EV-signed file now accrues reputation exactly like an OV-signed one, so EV ($400+/yr) buys
what OV (~$150–300/yr) buys. Signing is **not** instant regardless — a valid cert stops the
*malware* warning immediately, but "unrecognized publisher" clears only as SmartScreen accrues
reputation from real download volume. Cert validity is capped at **460 days** (recurring
line item) and private keys must live on an **HSM or hardware token**. The **Store MSIX route
needs no certificate at all** — Microsoft re-signs. `Distribution.md` §4.3 was corrected
2026-09-27; it previously recommended EV.
### Distribution and licensing are independent
A **Store-distributed** app may still verify licenses with **Polar** (policy 10.8.1
explicitly permits a secure third-party purchase API for non-game PC products; the box is
ticked in Partner Center). So "should we use the Store?" does **not** imply "should we drop
Polar?" — the only combination that removes Polar is Store + Store IAP, and that is a
*licensing* choice which happens to ride on a *distribution* decision. (The monetisation
posture itself is unchanged and lives in the Monetization section above: free gets
everything; the branding flash is the only paid delta.)
### ⛔ "Only when the user says so" is enforced by Windows, not by us
Camera/mic consent is layered: Store policy **10.6** forbids circumventing OS checks; the
**Windows desktop-app camera/microphone toggle** (Settings → Privacy & security, since Win10
1903) is the gate; hardware lights and tray indicators are the OS's disclosure mechanism. So
**route everything through the standard MF/DShow/WASAPI paths and we never write that
certification ourselves** — the OS does it and gets neither our credit nor our blame.
⚠️ **Untested:** whether that toggle reliably gates a **DShow webcam and a capture card**.
Microsoft's own support page admits desktop apps "might still be able to access your camera
or microphone even when these settings are turned off." This is **not** a certification
failure (we declare `webcam` honestly and use supported APIs) but it is a real user-trust
issue — so **no privacy-forward marketing copy until it is measured.** Guard note added to
`MARCOM.md`.
### ⛔ Durable invariant: chat is rendered, never stored
`TASK 49`'s profanity filter is **local, on-device, opt-in, non-persistent**, with a
**user-supplied word list — never a hardcoded slur list baked into the binary** (unpleasant
artifact, false positives across dialects, extractable). It must **never match the SuperChat
amount or any reward field** — those are financial data under policy 10.5.5.
The non-persistence half is not just hygiene: it is the load-bearing half of the **11.12 UGC
certification answer** (mirrored YouTube chat is moderated at industrial scale upstream; ~1.6B
comments removed in Q1 2026 alone, child safety the #2 reason at 124.6M). **No future feature
— chat history, moderation logs, analytics, crash payloads — may quietly break that
assumption.** If one is ever proposed, the 11.12 position must be re-argued first.
### Minors
The **operator is 13+ by construction**, not by promise: LlamaCasty requires a YouTube
account to stream, and YouTube's ToS sets the account floor at **13** (regional variants reach
14; 13–17 need parent/guardian permission; under-13s are on supervised/YouTube Kids accounts).
So a COPPA theory premised on collecting PI from under-13s has no purchase against the
operator. The residual is **viewers** — still possible on supervised family accounts — and it
is weak: live chat is restricted there, YouTube moderates before we see it, and we persist
nothing. COPPA is very unlikely to bind (not child-directed), but note the enforcement trend
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
persisted chat or reward payloads."