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
+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".