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.
4.6 KiB
TASK 49 — Chat profanity filter (local, opt-in, non-persistent)
Catalog:
TASKS.md— status and requirements live here. Research:TASKS/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:
- YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has something to attach to
- Competitive parity — overlay tools in this lane generally offer it
- It is a reasonable reading of 11.12's proactive-detection language
- 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:
- 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. - 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.
- 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.
- 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.Settingspattern — 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
AppLogcheckpoints must not capture message text - ☐ Correctness over performance: a few hundred comparisons per message is nothing
- ☐ Non-ASCII and unicode case handling — a naive
ToLowerInvariantwill 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.mditem 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.