feat(profile): the free budget is pixels and fps, enforced once — with a dev-only release hatch

The paid delta is output quality, so the rule needs to be measurable rather than a word:
921,600 pixels and 30 fps, fail-closed. Models/OutputProfile.cs is that rule as pure
policy — a permitted tier passes through untouched (Pro, or the free tier's own 720p30),
a disallowed one resolves to the best tier the budget allows and carries ITS bitrate, so
a capped user is never offered an 8 Mbps budget for a 720p frame.

Applied once, in BuildEncoderOptions. That is genuinely one place: the stream and the
local recording are two output blocks of ONE ffmpeg process fed by one EncoderOptions,
so there is no second encoder to forget. (commercial.md claimed "both encoders — never
two checks"; the code is stronger than the doc, and the doc is corrected.)

Developer override, as asked: a "Developer mode — all resolutions" checkbox in App
Settings, bound to MainViewModel.DeveloperMode, which feeds HasProOutput. It is not
hidden in a release build — Controls/DeveloperPanel's Page and Compile items are removed
when Configuration != Debug, and OverlayHost adds it at runtime inside #if DEBUG, so a
shipped exe has no BAML for it at all. The setter also refuses the value when
DevBuild.IsDevelopment is false, which is now the single source for that question (the
scoreboard partial had its own duplicate const).

Two traps worth their lines: a `const bool` dev flag makes `if (!Flag) return;` an
unreachable-code warning in one configuration or the other (CS0162), so DevBuild uses
static readonly; and a Condition on an ItemGroup is invisible to a test reading
item.Attribute("Condition") — read the parent.

Tests: 14 pass across OutputProfileTests, AssemblyIdentityTests and StorePackagingTests.
The invariant test is the one that matters: iterate every shipped tier under a free
entitlement and assert none emits more than the budget, so a future 4K tier fails here
instead of shipping to everyone. App builds Debug and Release at 0 warnings.
This commit is contained in:
2026-11-01 07:11:49 -08:00
parent abfab25685
commit 27fc6fb507
16 changed files with 363 additions and 31 deletions
+19 -19
View File
@@ -1,40 +1,40 @@
# HANDOFF — 2026-11-01 (the exe is LlamaCasty.exe; the depot folder builds and verifies)
# HANDOFF — 2026-11-01 (exe renamed, depot builds, output rule enforced)
## State
- Branch `main`, level with `origin` through `8f79b20`; this unit (the artifact rename) is local.
- **The shipped exe is `LlamaCasty.exe`.** `AssemblyName` renamed; four pack URIs fixed (the docs
said three — `KeepBroadcastDialog.xaml:5` was the fourth); manifest, MSIX script and
`StorePackagingTests` follow. Guarded by `ytLive.Tests/AssemblyIdentityTests.cs`.
- Branch `main`, level with `origin` through `8f79b20`; this unit is local.
- **The shipped exe is `LlamaCasty.exe`** (`AssemblyName` renamed; four pack URIs fixed — the docs
said three, `KeepBroadcastDialog.xaml:5` was the fourth). Guarded by `AssemblyIdentityTests`.
- **`scripts/publish.sh`** stages `artifacts/steam-stage` and refuses to finish if `ffmpeg\`, the
licence, privacy or notices file is missing, or test assemblies leaked in. Output 257 MB.
- **Distribution is Steam-exclusive** (creator ruling 2026-10-31): free app + paid edition as a
Steam **DLC**; the **$10/month** plan sold off-Steam by Polar and *stated, never sold*, in-app.
`commercial.md` → Channels is policy; `Distribution.md` is depot mechanics.
- **The output-profile rule is built and enforced**: `Models/OutputProfile.cs` (921,600 px / 30 fps,
fail closed) resolved once in `BuildEncoderOptions`, which feeds the one ffmpeg process writing
both outputs. `MainViewModel.DeveloperMode` bypasses it in Debug only.
- **Distribution is Steam-exclusive** (2026-10-31): free app + paid edition as a Steam **DLC**; the
**$10/month** plan sold off-Steam by Polar, *stated, never sold*, in-app.
- **Entitlement is `steamDlcOwned || polarKeyValid`**; `steam_api64.dll` ships beside the exe and
the app must start without Steam, falling back to free.
- **Site is live with Steam** (`a9d07ce`). `www.llamacasty.com` 301s to the apex.
- **Site live with Steam** (`a9d07ce`); `www.llamacasty.com` 301s to the apex.
## Next action
1. **OutputProfile gate** — ≤921,600 px / ≤30 fps for the stream **and** the recorder, greyed tier
entries, plus the TASK 53 geometry tests that pin every tier.
2. Then the **entitlement seam** (`IEntitlement`, Steam DLC path behind a fake, Polar path) and the
licence UI.
3. Steamworks: App ID waiting on identity verification; then depot, store page (public 2 weeks
before release) and the DLC app.
1. **Greyed tier entries + entitlement wiring** — the rule is in place and tested
(`Models/OutputProfile.cs`); the dropdown still shows every tier as equally available, and
`HasProOutput` is `IsPremium || DeveloperMode` with no Steam DLC path yet.
2. Then the **entitlement seam** (`IEntitlement`: Steam DLC via `steam_api64.dll`, Polar lease).
3. Steamworks: App ID waits on identity verification; then depot, store page and the DLC app.
## Landmines
- **⛔ `ExcludeFromSingleFile="true"` on the ffmpeg content** — without it the publisher bundles
libav\*.dll into the exe and the app ships unable to find ffmpeg. Recipe in `MyMistakes.md`.
- **Dev affordances are compiled out, not hidden** — `DeveloperPanel` is removed from the Release
build; `Helpers/DevBuild.cs` is the single source for the flag (never `const`).
- **Nothing may gate a feature.** The only paid delta is the output profile; `ShowBrandFlash` stays
`false` and entitlement must never enable it.
- **No checkout inside a Steam build** — the Wallet rule. The monthly is named, never sold, in-app.
- **The org access token never ships.** `organization_id` is public; the token is not.
- **The site publishes the model, never the numbers**, and never takes payment or links a checkout.
- **No self-updater and no version banner**: Steam updates the app.
- Pre-existing: the **full test suite hangs** (WASAPI teardown) — per-class runs only; and
`ytLive.Tests` carries 3 xUnit analyzer warnings the app-only gate does not see.
- Not every hex is a theme colour — `BorderColorPalette`/chat defaults are USER DATA; on-stream
graphics come from `OutputPalette`. A red settings control is still a bug.
- Pre-existing: the **full test suite hangs** (WASAPI teardown) — per-class runs; `ytLive.Tests`
carries 3 xUnit warnings the app-only gate does not see; a red settings control is still a bug.