ai.md 1,308 → 228 lines. No prose rewritten: every block was moved as-is so nothing could be lost in translation, then replaced by a pointer. New files: - docs/ai/environment.md — LAN/SSH + Gitea push recipe, Windows dotnet build/run, test-suite map, hermetic real-MainWindow test rule - docs/ai/product.md — brand/naming, AI transparency, GPL licensing prohibitions, free-forever monetization, auth/account assumptions, packaging & distribution - docs/ai/youtube-api.md — go-live ordering, the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, chat insert shape - docs/ai/overlays.md — social bar, native alert box (animations, clip, ticker), chat box conventions - docs/ai/ui-surface.md — control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, theme/properties conventions ai.md keeps the response/derivation rules, the architecture table, the four existing invariant stubs, and one pointer per moved subsystem. docs/ai/index.md routes every file with a "read when" column. References falsified by the move were fixed in the same commit: AGENTS.md build note → environment.md → Run, and the "Related" footers in frame-pipeline.md / output.md / capture.md now point at the owning files. Docs-only unit: scope-check run, no build (no source touched).
415 lines
30 KiB
Markdown
415 lines
30 KiB
Markdown
# Product: brand, licensing, monetization & distribution
|
||
|
||
What the product is called, what it promises, what it may never do legally, and how it ships.
|
||
Read before branding, marketing, pricing, licensing or packaging work.
|
||
|
||
## Brand
|
||
|
||
Brand palette and official assets live in `MARCOM.md` (gitignored — read it for hex values,
|
||
asset file names, and marketing copy). The brand red `#e94560` is the primary accent used
|
||
throughout the UI.
|
||
|
||
### Product name vs repo/assembly branding (2026-09-14)
|
||
|
||
The product is **llamacasty** (llamacasty.com, llamachile.tube, `llgit.llamachile.tube`). The
|
||
repo path, csproj `AssemblyName`/`RootNamespace`, DB/log paths (`%APPDATA%\ytLlive\...`),
|
||
and most internal namings are the legacy **ytLive/ytLlive** — a re-brand that never renamed
|
||
the internals. Treat the two as separate: external/user-facing language says "llamacasty",
|
||
code/assembly/repo names stay `ytLive`. If a full internal re-brand is ever done, this note
|
||
and the UI locator (Premium/wordmark) are the checklists; it is NOT a goal pre-1.0.
|
||
|
||
**Say "LlamaCasty" when reporting to the creator** — never "ytLive" in prose. `ytLive` is
|
||
the internal repo/assembly/namespace name only. Exact current split:
|
||
|
||
| surface | name | note |
|
||
|---|---|---|
|
||
| product / site / marketing | **LlamaCasty** | llamacasty.com, llamachile.tube |
|
||
| repo dir, csproj, `RootNamespace` | `ytLive` | 230 files: `namespace ytLive.*` |
|
||
| `AssemblyName` → shipped exe | `ytLive` | `ytLive.exe` |
|
||
| app data dir | `%APPDATA%\ytLlive` | **capital L**, drifts from `ytLive` |
|
||
| `Distribution.md` tells customers | `LlamaCasty.exe` | ⚠ **mismatch vs the real `ytLive.exe`** |
|
||
|
||
**Consequences, so nobody re-hunts them:**
|
||
- Renaming `<AssemblyName>` to `LlamaCasty` is the cheap win (the customer-visible exe
|
||
name matches what `Distribution.md` already promises; namespaces can stay `ytLive.*`).
|
||
It is **4 lines**, but it hard-breaks three pack URIs that spell the assembly name:
|
||
`MainWindow.xaml:14` (the **window icon** — taskbar/Alt-Tab), `OverlayHost.xaml:96`,
|
||
`RenameRecordingDialog.xaml:5`. Miss those and you ship a broken icon. Queued in TASKS.md
|
||
under the shipping task, not done.
|
||
- Renaming the `%APPDATA%\ytLlive` folder is **breaking** — it would orphan every existing
|
||
user's layout DB, `Assets` and settings. Never do it casually; needs a migration.
|
||
- No code compares its own assembly name (no `GetExecutingAssembly().GetName().Name` checks),
|
||
so the assembly name is otherwise free to change.
|
||
|
||
### AI transparency
|
||
|
||
This project is built through AI-assisted pair-programming. All code is generated
|
||
under the supervision of an experienced developer with decades of coding experience.
|
||
Architecture decisions, product direction, and quality gates are human-owned;
|
||
AI accelerates implementation. This is a testimonial to successful human-AI
|
||
collaboration, not autonomous code generation.
|
||
|
||
## Licensing — do not violate (GPL-3.0 open source since 2026-10-09; see `LICENSE`, `ADDITIONAL-TERMS.md`, `THIRD-PARTY-NOTICES.txt`)
|
||
|
||
**LlamaCasty is free software under GPL-3.0-or-later** (creator rulings 2026-10-09 — free forever, public
|
||
repo, "GPL-3 with attribution"). Copyright (C) 2026 Gramps. `ADDITIONAL-TERMS.md` carries the three
|
||
§7 terms: **7(b)** every copy keeps "Based on LlamaCasty by Gramps — https://llamacasty.com" in its
|
||
About screen; **7(c)** modified versions are marked and renamed; **7(e)** no trademark rights. The
|
||
**icon + logo files** (`Assets/llamacasty-icon.png/.ico`, `Assets/llamacasty-logo.jpg`) are **all
|
||
rights reserved, outside the GPL** — verbatim copies may carry them, forks must replace them (this is
|
||
what keeps the merch brand protected). The scene backgrounds + default alert video are AI-generated
|
||
**placeholders** (creator: made "with the intent of individual creators to immediately replace with
|
||
their own images") — they go out under the GPL with the code, no reservation. `LICENSE` + `ADDITIONAL-TERMS.md` ship beside the exe
|
||
(csproj `None` items); the About panel shows `THIRD-PARTY-NOTICES.txt`, whose header carries the
|
||
LlamaCasty license notice (GPL §5(d) "Appropriate Legal Notices"). **Source offer (§6):**
|
||
`https://llgit.llamachile.tube/gramps/LlamaCasty` (the Gitea repo; creator toggles it public — the
|
||
old `gramps/ytLlive` path 301-redirects there), recorded in the notices header + README.
|
||
|
||
Every bundled component must stay GPL-3.0-compatible (MIT/BSD/Apache-2.0/LGPL/public domain all are;
|
||
**GPL-2.0-only and proprietary are not**; Apache-2.0 is fine with GPL-3 but NOT with GPL-2). The
|
||
rules below predate the GPL switch; where their *reason* was "proprietary product" it is noted, but
|
||
the rule itself still stands for its other reasons. **NEVER:**
|
||
|
||
- **Swap the FFmpeg pin casually** — the BtbN `lgpl-shared` pin is the verified build. A GPL FFmpeg
|
||
build would now be license-compatible (the old "GPL contaminates the paid product" reason is gone),
|
||
but changing builds means re-verifying encoders, and libx264 carries no H.264 patent grant (Cisco's
|
||
covers OpenH264). Keep `lgpl-shared` unless a task says otherwise.
|
||
- **Distribute the static lgpl build** — LGPLv2.1 §6 wants relinkable object files for static linking.
|
||
The **shared** (dynamic-DLL) build sidesteps that: compliance is "license text + source offer +
|
||
unmodified binaries". The pin is `lgpl-shared`; when the pin is refreshed, keep the shared variant.
|
||
- **Use BtbN's `nonfree` variant** — it adds fdk-aac (Fraunhofer code licensing). The native FFmpeg AAC
|
||
encoder is fine (no Fraunhofer code) but grants no AAC patent license — accepted low-risk posture for
|
||
RTMP→YouTube, since encoder vendors cover their implementations (Cisco OpenH264, NVIDIA NVENC, Intel
|
||
QSV, AMD AMF).
|
||
- **Link FFmpeg into the app** — it stays a separate subprocess fed frames over a pipe; that separation
|
||
keeps the app's own code out of LGPL reach.
|
||
- **Drop `THIRD-PARTY-NOTICES.txt`** from the shipped app or the About screen, or alter the FFmpeg
|
||
copyright/LGPL notices inside the downloaded binaries. Automating the download counts as distribution
|
||
— the obligations are not optional.
|
||
- **Pin to a moving target** — the `latest` BtbN release tag floats. Only immutable autobuild tags give
|
||
a reproducible source offer. Record the tag + variant beside the URL (TASKS.md) every time the pin moves.
|
||
- **Use non-CC0 icon art** — the social bar's bundled SVG logo path data comes from **Simple Icons**
|
||
(CC0 1.0, public domain — see `THIRD-PARTY-NOTICES.txt`). Replacing or adding logos must stay CC0 or
|
||
another public-domain source; a logo asset under an incompatible or attribution-heavy license
|
||
would complicate every redistribution. The app's icon + logo (`Assets/llamacasty-icon.*`,
|
||
`Assets/llamacasty-logo.jpg`) are the creator's own art — all rights reserved, outside the GPL
|
||
(`ADDITIONAL-TERMS.md`).
|
||
- **Ship an ML model without clean provenance** — the background-effects model (TASK 51) is Google's
|
||
official MediaPipe Selfie Segmenter download (Apache-2.0, model card), converted by our own script.
|
||
Model-zoo re-hosts (e.g. PINTO #109 = ML Kit's model, extracted) are off-limits. Apache-2.0 §4
|
||
obligations (license notice + "we converted it") live in `THIRD-PARTY-NOTICES.txt`.
|
||
- **Forget the v1 license-texts gate** — `THIRD-PARTY-NOTICES.txt` links the canonical license texts; at
|
||
**v1 (GA)** the full texts of every license it names MUST ship alongside it (TASK 4 requirement 9 is the
|
||
release blocker). Queued early is wrong; the release pass owns it.
|
||
- **Break the in-app About requirement** — since TASK 11 the About overlay is the app's in-app creator
|
||
hub: the real logo, the creator links (YouTube channel, Mastodon, Buy me a coffee, and the greyed
|
||
"Unlock Premium" seam whose billing URL is tabled), and a **Licenses & legal** sub-panel that loads
|
||
`THIRD-PARTY-NOTICES.txt` from the executable directory and renders it in a scrolling panel —
|
||
**never the OS viewer, no Notepad, no browser tab**. The premium/coffee link constants live in
|
||
`MainViewModel` (`PremiumUrl`, `CoffeeUrl`, `ChannelUrl`, `MastodonUrl`); filling in the billing URL
|
||
lights up the premium button. Do not reintroduce an "open the notices in Notepad" button.
|
||
|
||
## Design Principle
|
||
|
||
> This software is so intuitive that even the most right-brained person can easily intuit and use it.
|
||
|
||
Apply this to every UI decision:
|
||
- One-click go-live with working defaults
|
||
- Prefilled YouTube defaults (RTMP URL, bitrate, resolution, latency)
|
||
- Visual/drag-and-drop scene building over property panels
|
||
- Every action produces a visible outcome — no dead ends
|
||
- **"We're not them" (2026-09-01):** assume the creator's hardware is mediocre, because it is.
|
||
Every feature spends the machine's budget once — never twice for the same result (record OR
|
||
stream, not both; one reusable stream; one webcam; two mixer inputs). If a feature only sings on
|
||
a high-end chassis, it doesn't ship — the OBS escape hatch is open by design.
|
||
|
||
## Monetization (design decision — the branding flash is the sword)
|
||
|
||
> **⛔ Superseded 2026-10-08/09 — free forever, no branding on the stream.** The credit is now
|
||
> **OFF by default**: `MainViewModel.ShowBrandFlash` (internal static, default `false`) is the
|
||
> master switch, and `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the presenter at
|
||
> construction and on every licence change. The code (`BrandFlashPresenter`, the FramePump
|
||
> `brandFlash:` seam, the preview layer) is kept, not deleted — creator ruling: "don't delete it,
|
||
> just make it conditionally displayed". `BrandFlashOutputTests` opt in by setting the switch
|
||
> before constructing a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default.
|
||
> Everything below is the historical paid-tier design.
|
||
|
||
Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar
|
||
product, discount code, or license key exists any more** — nothing bills today; Polar is dormant.
|
||
Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model.
|
||
|
||
Free forever: **all features unlocked for everyone** — no feature lock, no paid tier, and since
|
||
2026-10-08 **no watermark either** (the credit is OFF by default; see the banner above). No
|
||
creator will tolerate a watermark, and as a good-guy developer we give everyone complete access
|
||
so no one can call us crooked. Money comes from gratuity/merch, not gating (TASK 54).
|
||
|
||
- **Free:** a periodic branding credit — "made with LlamaCasty!" in **full-frame neon** (white core,
|
||
feathered red/blue halo with the channels split in opposite directions), at a **random position
|
||
inside the frame on every presentation** (single pre-measured line, so it never wraps; travel range
|
||
is `0 … 1920−textWidth`), held **2s** (500ms fade in, 1000ms full, 500ms fade out), first credit
|
||
~5s after go-live then **every `rand(30s)+30s`** (30–60s), on the live output **and** local
|
||
recordings. An always-on watermark can be cropped or covered; an intermittent full-frame flash can't
|
||
be cropped and is impractical to edit around on a live feed.
|
||
**The flash is also the free tier's billboard** — every free stream advertises LlamaCasty to its own
|
||
viewers; the free tier is distribution, not compromise.
|
||
Implemented as `Services/Compositor/BrandFlashPresenter.cs` (raster + envelope + placement + licence
|
||
gate), published to **both** the frame pump (`FramePump(brandFlash:)` → the compositor's per-frame
|
||
`flashFrame` slot) and the preview (`BrandFlashElement`, bound to `BrandFlashImageSource`) from the
|
||
**same** `VideoFrame`, so the preview cannot drift from the broadcast. Deliberately NOT the
|
||
`flashFrame` parameter of `SceneGraph.GetBakedBase` — `SceneRegion.Matches` compares element ids
|
||
only, so a credit folded into the cached bake would freeze there and never expire.
|
||
**Pre-GA posture — REVERSED 2026-09-26 (TASK 36 shipped).** This used to render *in the preview
|
||
only*, never on the broadcast, so test VODs stayed clean. The creator ruled that the free tier must
|
||
be honest advertising: it now composites into the recording and the stream. Test VODs from an
|
||
unlicensed instance carry the credit — use a license key or the paid build for clean captures.
|
||
**Escalation model (2026-09-01, creator decision):** the cadence is *obnoxiously* self-promoting.
|
||
License activation still flips exactly one bit: `IsPremium` → flash off. Nothing else changes
|
||
between free and paid, ever.
|
||
**Cadence is APP-LIFETIME, not go-live (creator ruling 2026-09-26):** the presenter is
|
||
`Start()`ed **once in the `MainViewModel` ctor**, not from `UpdateLiveVisuals()`. It used to be
|
||
gated on `IsLive`, which meant a recording made without ever going live carried no credit — the
|
||
creator's ruling: *"the made with llamacasty flash should appear in all scenes, not just live"*
|
||
and *"should also appear in recordings"*. One start now covers every scene, the preview, the
|
||
stream and the recording, because go-live and local recording are the **same `FramePump`**
|
||
(`Streaming.Operations.cs:68` and `:199` both call `StartAsync` with the same `brandFlash:`
|
||
delegate) — verified, not assumed; there is no second encoder path needing a "redirect". The
|
||
licence gate needs no live branch: `IsPremium`'s setter pushes `Enabled` from anywhere.
|
||
**Consequence of app-lifetime — the premium edge restarts the timer.** `Enabled = false` stops
|
||
the `DispatcherTimer` (cutting the advertisement mid-credit). Because `Start()` is no longer
|
||
called per go-live, nothing would ever restart it, so a key entered mid-session would leave the
|
||
credit dead until the process restarted. The setter therefore restarts it when re-enabling while
|
||
`_running` (`IsCadenceTimerEnabled` is the test seam).
|
||
**Test-arithmetic trap, hit again 2026-09-26:** `Advance(5.1)` in ONE call cannot observe a
|
||
credit — `Advance` both *opens* the presentation and *ages* it by the same delta, so a single
|
||
5.1s step blows clean through the 2s window. Always step in 1/30s increments like the real timer
|
||
(the `Step` helpers in `BrandFlashOutputTests`).
|
||
- **Paid:** branding flash removed. `BrandFlashEnabled` is now a **derived, non-assignable**
|
||
`!IsPremium` and the presenter's own `Enabled` gate is re-checked on every frame, so the
|
||
entitlement changing mid-credit cuts the advertisement on the next tick rather than letting it
|
||
finish. That's it. No feature gating. Alerts, social bar slots, voice filters, TRAX, recording —
|
||
everything is free. **How `IsPremium` is obtained is a billing concern and has changed twice;
|
||
what it gates has not.**
|
||
- **Pricing: ⛔ SUPERSEDED 2026-10-08 by the free-forever ruling (TASK 54).** Retained as
|
||
reasoning only: `$10/mo` subscription or `$400` lifetime, no annual, ruled 2026-09-27 and
|
||
overtaken ten days later. If a paid tier ever returns, these are the numbers to argue from.
|
||
**How `IsPremium` would be obtained is a billing concern; what it gates is not.**
|
||
- **Billing: ⏸ dormant — no paid tier exists** (free-forever ruling 2026-10-08, TASK 54). The
|
||
Store IAP plan of 2026-09-27 was never implemented (`Windows.Services.Store` appears nowhere
|
||
in the repo) and was withdrawn with the Store route on 2026-10-22. If a paid tier ever
|
||
returns, the provider is **Polar** — merchant of record for VAT/sales tax, 5% + $0.50 Starter,
|
||
**$50 already paid**; `Distribution.md` §4.2/§4.5 holds the design. ⚠️ No Polar product, org
|
||
ID, key prefix, or discount code exists (`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/
|
||
`LLAMA50`, the customer-portal validate endpoint, and the 14-day offline-grace machine are all
|
||
gone).
|
||
- **Support (creator's model, corrected 2026-09-01):** support = email + GitHub issues — the
|
||
once-planned in-app bug-reporter is **out of product** (TASKS.md → closed list). Most queries are
|
||
how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with
|
||
emergency patches; not a 24/7 service promise. There is **no licence chatter at all** — the app
|
||
is free, there are no keys, and no user sees licence UI. TASK 36 item 5 (expiry reminder) is
|
||
struck.
|
||
|
||
**Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1
|
||
ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by
|
||
design, for **every** creator, free and ungated (the only subscriber swap remains the branding-flash
|
||
removal above). It has three layers, all free — this is the product's differentiator, not a paid tier:
|
||
|
||
- **Reward-event capture (the data foundation).** The live-chat poll (`YouTubeChatService.Poll`)
|
||
receives every reward event over `liveChat/messages` and — since TASK 43 (2026-09-24) —
|
||
**decodes all six types**, not two: `superChatEvent`, `superStickerEvent`, `newSponsorEvent`,
|
||
`membershipGiftingEvent`, `giftMembershipReceivedEvent`, `memberMilestoneChatEvent` land as
|
||
`ChatMessage.Kind` (`ChatEventKind`) with their payload fields (display string, sticker
|
||
description, gift count, gifter name, milestone months); free subscriptions emit NO event
|
||
(API fact — a sub mention is a plain text row). The remaining half of the original capture
|
||
plan stays open (TASK 3 item 20): persist each event to the canonical SQLite `RewardEvents`
|
||
table (broadcastId, type, timestamp, amountMicros, currency, tier...) + `superChatEvents.list`
|
||
(30-day lookback) backfill — that persistence is what the session report and the journey
|
||
tracker read. Alerts did not wait on it: the shipped alert path (TASK 43) renders directly
|
||
from the in-memory `ChatMessage` feed.
|
||
- **Session / broadcast report.** A session is created at Go Live; the poll + End roll that session's
|
||
reward totals (new members, Super Chat $, stickers $, gifts, milestones). This is the in-app
|
||
replacement for the emailed external "stream activity report" — the incumbents (Streamlabs et al.)
|
||
email near-useless post-stream summaries; we track the same data natively and maintain it.
|
||
- **Journey tracker (TASK 39, slice 1 shipped 2026-09-22).** A **"YPP"** tab below the
|
||
Stream Settings pull-out (Live screen right edge) shows the connected creator's position
|
||
toward each YPP tier threshold. Slice 1 = current-scope data only: `channels.list?mine=true
|
||
&part=statistics,contentDetails` (subs/views/videos + uploads-playlist id) +
|
||
`playlistItems.list` for 90-day public uploads. Every refresh appends
|
||
a `YppSnapshot` to SQLite (schema v10) — the history a later slice needs for velocity/ETA.
|
||
**The `auditDetails` part is OUT (fixed 2026-09-23):** readding it 403s the WHOLE request —
|
||
that part alone requires the `youtubepartner-channel-audit` scope no app for normal creators
|
||
should request (the standing flags sat unreadable since slice 1 shipped; the app's `youtube` +
|
||
`youtube.force-ssl` scopes were never sufficient for them — the "no re-consent" slice-1 claim
|
||
was wrong for that part). Channel standing now deep-links to the Earn page; API stats come from
|
||
`statistics,contentDetails` only. YPP/API failures log the response body's `error.reason`.
|
||
**YPP thresholds are versioned, date-aware DATA, never constants** — the Tier-2 bar doubles
|
||
for new applicants on **2027-02-01** (long-form 4,000 → 8,000 qualified hrs / 365d; Shorts
|
||
10M → 20M / 90d), while the Tier-1 fan-funding bar (500 subs / 3,000 hrs / 3M Shorts) is
|
||
unchanged. Hardcoding the pre-2027 numbers would ship the wrong ETA. Watch-hours / Shorts /
|
||
velocity / ETA are **slice 2**: Analytics-API scopes (`yt-analytics.readonly`, additive
|
||
`yt-analytics-monetary.readonly`) behind the `IChannelStatsProvider` seam (additive
|
||
capability, requires an OAuth re-consent). Compliance items no API exposes (2FA, AdSense
|
||
linkage) are in-app self-reported checkboxes with deep links; channel standing (exposed only
|
||
via the partner-audit scope) deep-links to the Earn page. Slice 2's once-daily refresh gate +
|
||
snapshot history were baked into slice 1.
|
||
|
||
**What was rejected:** always-on watermark (obscurable — replaced by the flash), hard stream-time
|
||
cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
|
||
routinely run 2-4 hours), soft-limit nagging, freemium feature tiers, and donation-only (relies on
|
||
the kindness of strangers). Resolution/quality ceilings stay rejected (fixed 2026-09-01, no longer
|
||
"deferred"): the free tier never loses quality — auto step-down (**TASK 33**) is a *protect the
|
||
stream* feature, never a monetization penalty.
|
||
|
||
## Auth gates Go Live, but not exploration
|
||
|
||
The app is fully usable without authentication: users can build scenes, add sources, compose
|
||
previews, and audition the software with zero commitment. But **going live requires authentication** —
|
||
it's the one capability gated behind YouTube sign-in. The sign-in should never pressure the user
|
||
("sign in (optional)", not a modal wall): the two-state top bar shows **Start Stream** (offline) /
|
||
**End Stream** (live), and the Start Stream dialog hosts the account — a saved session appears as
|
||
the default with "Change Account"; with none saved, a "Sign in to YouTube" button starts OAuth and
|
||
the Start button stays disabled until signed in.
|
||
|
||
## Account assumption (do not build an account setup flow)
|
||
|
||
Connecting uses Google OAuth ("Sign in with Google") to link an **existing** YouTube creator
|
||
account. ytLlive **never creates or sets up accounts** — that is YouTube's job. If the creator has no
|
||
YouTube channel, they go to YouTube first. This assumption is explicit and must never be silently
|
||
replaced by an in-app account-creation step. Zero state = the Start Stream dialog's "Sign in to
|
||
YouTube" button; going live is unreachable until an account is connected.
|
||
|
||
## Windows packaging & distribution (⛔ REVERSED 2026-10-22 — direct + Steam; Store dropped)
|
||
|
||
**The Microsoft Store route is dead.** A brand name on the Store's *publisher* line requires a
|
||
**Company** account, which requires a registered legal entity (an LLC — ~$85 New Mexico with a
|
||
~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an Individual account to
|
||
a Company account later, and an Individual account publishes under the verified legal name. The
|
||
creator will not fund an entity for a nameplate, so the 2026-09-27 Store ruling was withdrawn on
|
||
2026-10-22. Store IAP was never implemented either — `Windows.Services.Store` appears nowhere in
|
||
the repo.
|
||
|
||
**The route now:**
|
||
|
||
1. **Direct download from `llamacasty.com`** — the app is **free forever** (TASK 54), so there is
|
||
nothing to bill in v1.
|
||
2. **Steam when $100 exists** — Steam Direct is $100 per product (recoupable after $1,000 AGR),
|
||
accepted categories include *Audio/Video Production* / *Player Tools* (OBS Studio is the
|
||
precedent), the **developer/publisher name is self-chosen** so the brand needs no entity,
|
||
5+ screenshots at 1920×1080 + capsule art, a build Valve runs, and a **2-week live page**
|
||
before release. Valve takes 30%.
|
||
3. **Polar is dormant, not deleted-in-principle** — chosen checkout/licence provider if a paid
|
||
tier ever returns; $50 already paid. `Distribution.md` §4.2/§4.5 holds the design.
|
||
4. **The Microsoft Store: not pursued.** The MSIX package stays as a working artifact and is
|
||
submitted nowhere. History: `TASKS/research-store-certification.md`,
|
||
`TASKS/task-48-distribution-msix.md`.
|
||
|
||
**Consequences that are architecture, not packaging detail:**
|
||
|
||
- **⛔ The updater is now MISSING, not deleted-by-design.** Velopack and its bootstrap were
|
||
removed on 2026-10-22 (TASK 48 slice 3) because the Store was going to deliver updates — that
|
||
premise is gone. An update path must come back, and
|
||
`ytLive.Tests/StorePackagingTests.cs` (which asserts no updater returns) must be rewritten
|
||
with the chosen updater rather than left to veto one.
|
||
- **The MSIX package still builds** via `scripts/package-msix.sh` (TASK 48 slice 2) — publish
|
||
`Release -r win-x64 --self-contained` into `artifacts/msix-stage`, copy
|
||
`Packaging/Package.appxmanifest` in as `AppxManifest.xml` + `Packaging/Assets/`, then
|
||
`makeappx pack` → `artifacts/LlamaCasty.msix` (159 MB, gitignored; `MSIX_PFX` signs it for a
|
||
local install). The manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with
|
||
`runFullTrust` + webcam + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in
|
||
`MyMistakes.md`. Logos come from the app's own artwork via `scripts/make-store-logos.py`.
|
||
- **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a direct
|
||
download, and it fixes the 2026-09-01 404). See [`docs/ai/output.md`](docs/ai/output.md) → "FFmpeg locator".
|
||
- **⛔ 64-bit Windows only (creator ruling 2026-10-09).** Both csproj files set
|
||
`<PlatformTarget>x64</PlatformTarget>` (the test project too — an AnyCPU test project
|
||
referencing an x64 app raises MSB3270 and breaks the 0-warning gate). Why: the bundled ffmpeg
|
||
is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a
|
||
32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway.
|
||
Public copy says "64-bit Windows 10 or 11".
|
||
|
||
**What does NOT change: the monetization posture.** The app is **free forever with no watermark**
|
||
(TASK 54, 2026-10-08), so `IsPremium` gates nothing in v1 and the branding flash is OFF by
|
||
default. `Monetization` above governs.
|
||
|
||
### ⛔ 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 2026-10-22 ruling
|
||
is to ship unsigned and accept the "unrecognized publisher" warning** until download reputation
|
||
accrues — buying a certificate is deferred, not rejected. `Distribution.md` §4.3 was corrected
|
||
2026-09-27 (it previously recommended EV).
|
||
|
||
### Distribution and licensing are independent — and the channel decision does not decide licensing
|
||
|
||
A **Store-distributed** app *may* still verify licences with **Polar** (Store policy 10.8.1
|
||
explicitly permits a secure third-party purchase API for non-game PC products), so "which
|
||
channel?" never implied "which billing?" — route B was a real option. The 2026-10-22 reversal
|
||
proves the point in the other direction: dropping the Store did not by itself restart Polar,
|
||
because the **2026-10-08 free-forever ruling** (TASK 54) had already removed the paid tier.
|
||
Polar is therefore *dormant*, ready if a paid tier ever returns, and nothing in v1 depends on it.
|
||
The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a
|
||
distribution decision.
|
||
|
||
### ⛔ "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."
|
||
|