docs(handoff): rewrite for pushed 0c55645 (ON-AIR armable signed-out Good Dog ruling + ONE PillRadio test)

This commit is contained in:
2026-09-20 16:14:35 -07:00
parent 0c556450cb
commit 523c17b7da
+45 -132
View File
@@ -1,145 +1,58 @@
# HANDOFF — 2026-09-20 (cross-table z-order bug fixed; REC/ON-AIR pills now radio-exclusive; defaults/current split queued as TASK 37)
# HANDOFF — 2026-09-20 (ON-AIR armable signed-out shipped + pushed; Good Dog ONE-test ruling)
## Branch / Commit State
`main` HEAD = **the cross-type ordering fix** (local — no push). This is a CONTINUATION of the
2026-09-18 drag-reorder work: the creator kept insisting the reorder "doesn't save" even after
commit `f91bf87`. Their repro was real and the per-table `SortOrder` theory was the actual mechanism.
`main` HEAD = **`0c55645` — feat(pills): ON-AIR armable signed-out; Start face reads "Sign In"
while armed signed-out (Good Dog 2026-09-20 ruling)** — **PUSHED** to `origin/main`
(`5a1993b..0c55645`, non-force). Working tree **clean** (all 6 units of the change committed).
**No push yet.** Working tree clean after the ordering-fix commit.
This is a continuation of the 2026-09-18 drag-reorder + 2026-09-20 radio-exclusive-pill work: the
ON-AIR setter could only light while `IsConnected`, so a signed-out creator found a **greyed pill
= dead end**. The ruling ships the armable signed-out pill.
## ✅ Committed — cross-type z-order: webcam between sources survives save→reload (2026-09-20)
## ✅ What shipped this session (commit 0c55645, pushed)
**The actual bug the creator reproduced** (`f91bf87` shipped the immediate-save at drop, but the
reorder still reverted on restart): `Source` and `WebcamSceneConfig` are separate tables with
**independent per-type `SortOrder` counters**. Save stamped sources 0..n and configs 0..m;
`LayoutStore.Load` appended **all Sources first, then all WebcamSceneConfigs**. A webcam dragged
between two sources was structurally unrecoverable — the live scene (Background, YouTube Chat,
Image, Web Resource-0 sources + one HD Pro Webcam C920 config) reverted the Webcam to the bottom
of the stack on every relaunch.
- **ON-AIR pill is intent, not account reality** — `OnAirPillOn` setter is **armable signed-out**
(greyed/disabled-until-`IsConnected` gate REMOVED; a pill that could not light offline was the
dead end the guard existed to forbid). REC-OR-live radio exclusivity is untouched — arming one
clears the other; the radio constraint lives at the pills.
- **Start button face is reality-aware**: `PrimaryStartButtonLabel` = `"Start"` while
`IsConnected || !OnAirPillOn`, else **`"Sign In"`** when an ON-AIR pill is armed signed-out.
Clicking Start while the ON-AIR pill is armed signed-out routes to **sign-in**, not a dead-end
go-live (no dead ends — creator ruling; see ai.md §Streaming pills).
- **One Good Dog test** (`ytLive.Tests/PillRadioTests.cs`): `PillRadioTests.Arming_One_Output_
Pill_Clears_The_Other` (radio-exclusive) **+** the signed-out armable-pill headless test
(`RealAppHost` + temp DB, no browser).
- **ai.md** updated in the SAME commit: tooltip wording + the armable-signed-out ruling
(2026-09-20) recorded; stale "greyed/disabled until `IsConnected`" fact replaced.
**Fix:** one shared z-space per scene. `Save` stamps both tables' `SortOrder` from the element's
index within `scene.Elements` (the non-source branches bump the counter too); `Load` merges the two
tables' rows **by that shared z** instead of appending tables. Legacy pre-unification rows can tie at
0 — ties break sources-first, so old data reads back exactly as it saved.
## Deferred / queued (recorded in TASKS.md, NOT folded into this change)
**Good Dog test** `CrossTypeReorder_BelowTheWebcam_SurvivesFreshReload` (`LayerReorderPersistenceTests`):
seeds the builder's real Live layout (4 sources + `HD Pro Webcam C920` config row), reorders
"Web Resource-0" DOWN below the webcam, calls `OnSceneElementsReordered`, then opens a **brand-new
`LayoutStore` on the same temp DB — the restart** — and asserts the cross-type order holds. **309/309
tests pass, build 0 warnings.**
- **TASK 37 — defaults vs current layout split**: DB holds static brandable `default` layout; the
`current` layout carries the build-id and only loads when it matches `BuildStamp.Id` — enables
one-click revert to factory defaults. Files: `TASKS/task-37-defaults-vs-current.md`.
- **Manual take on webcam-between-sources order** (cross-type z-order fix `94e114b` already
committed/pushed): relaunch, live view, drag Web Resource-0 below the webcam, restart, expect it
pinned. Closes the bug with the builder before any further streaming work.
- **Build-id-on-exit** demand: still queued for a later slice (do not fold in).
**Also committed:** TASK 37 queued — **defaults vs current layout split** (creator directive
2026-09-20): the DB should hold the static brandable `default` layout; the `current` layout carries
the build-id (loaded only when it matches `BuildStamp.Id`), enabling one-click revert. Conceptual
shape `scene[0-4].layerList.[default|current].elementList`; config-file vs second-rows-in-DB not yet
decided (`TASKS/task-37-defaults-current-split.md` has both + facts). **Process rule established:**
capture out-of-scope needed work in TASKS.md rather than bolting it onto the in-flight change.
## Critical working rules (failure cap)
## ✅ Committed — REC/ON-AIR pills are radio-exclusive (2026-09-20)
- **Good Dog = ONE integration test per change.** Do not pile up multiple tests for one guard.
- **Scope lock:** declare the exact file list BEFORE editing; run `scripts/scope-check.sh` with the
declared list before commit. No "while I'm here" edits.
- **Run from WSL** always uses the Windows dotnet host (`/mnt/c/Program Files/dotnet/dotnet.exe`)
with **quoted** paths — a path containing a space left unquoted was the #1 tool-spin cause this
session. Test filter runs go through `dotnet vstest <dll> --filter …` (Windows DLL path), NOT
`dotnet test` (WSL re-downloads the windowsdesktop packs + double-restores).
- **Never type the username in paths.** Canonical root is `$(pwd)`. Byte-verify every edit anchor
(node `fs` on `path.join(R, p)`) — the phantom-tree reads wasted this whole session.
- **ai.md + code in the SAME commit**; rewrite HANDOFF.md at session end or interruption.
**Gap found:** the record-OR-live ruling (2026-09-01) was documented but the "pills = radios" UI
constraint never landed — the two pills were independent toggles, so a creator COULD arm both and
`StartSession`/`BuildEncoderOptions` would happily dual-encode (RTMP + `-f mp4`) — the exact thing the
ruling forbids (a concurrent disk write streams from the same sustained ingest Kbps and cripples the
stream on mid-range chassis; "the VOD is already the copy"). Fix: mutual clear in both pill setters
(`RecordPillOn`/`OnAirPillOn` in `MainViewModel.Streaming.cs`) — arming one clears the other (silent
mutual clear, creator's choice). The dual-block ffmpeg machinery stays generic/unreachable by design.
Test `PillRadioTests` (RealApp + temp DB): REC→ON-AIR clears REC; ON-AIR→REC clears ON-AIR; single
pill still arms Start. **310/310 pass.**
## Where the machine facts live
## ✅ Committed earlier — layer-list drag-to-reorder persists (2026-09-18, `f91bf87` + below)
`StagedScene.Elements` reorder (RemoveAt/Insert) bypasses SceneGraph's mutation surface, so the drop
must be trapped + persisted. Fix (2026-09-18): the drag sets `_dragReordered` and `EndListDrag`
funnels the drop through `MainViewModel.OnSceneElementsReordered()` (Sources.cs) =
`SceneGraph.InvalidateBake(staged) + SaveLayoutNow()` — an IMMEDIATE save at drop (no debounce
window). The single write stores BOTH the layer list (SortOrder) AND the preview layout (X/Y/W/H in
the same rows). Tests `LayerReorderPersistenceTests` (real App + temp DB): (1) exact code-behind
mutation + geometry assert; (2) **real-gesture** test `RealMouseDrag_OnTheLayerList_PersistsTheReorder`
(`SetCursorPos` + `mouse_event` on a shown Topmost window) — needs an interactive desktop session
(covered/locked session = pointer no-op = flake). Both green in the 309. **This fix was correct but
INSUFFICIENT — the cross-table bug above is what the builder actually hit.**
## ✅ Committed earlier — webcam app-default gate + attainability (`9d00955`, `75723ac`)
Webcam is an **app-level resource** — ONE app-wide default, usable in every scene that can host it,
NOT blocked by other scenes' placements (creator ruling; supersedes TASK 26). Per-scene gate
`CanAddWebcam` = scene has no config && attainable (`CameraManager.IsRunning(_webcam.DeviceId)` —
a live lock, not a saved identity). Add places the default directly (no picker). Identity survives
last-placement removal; startup lays an app-wide BASE lock (refcount keeps the session alive);
adopts a solo camera as default. Dynamic why-gray tooltip. Tests: `WebcamMenuGateTests`,
`WebcamStartupResourceTests`. The DB truth lives at `%APPDATA%\ytLlive\ytLlive.db` (NOT `layout.db`).
## ✅ Committed earlier — Rename Recording dialog taller + full-screen hook unhooked (2026-09-18)
`RenameRecordingDialog.xaml` Height 230 → 276 → **304** (the +20% revealed the Cancel/Save row also
obscured); Save button named `SaveRecordingButton`. Test `RenameRecordingDialogSizingTests` asserts
textbox bottom + Save button bottom inside the client area. **Teardown leak fix:** `Shutdown()` now
calls `_fullScreenDetector.StopWatching()` — the global `EVENT_SYSTEM_FOREGROUND` hook was never
unhooked, invoking a GC'd `WinEventProc` on the next foreground event → test-host abort.
## ⚠️ Open items
- **Build-id on command-line exit (creator demand, 2026-09-20):** when the app exits under
`dotnet run`, output the build number (`BuildStamp.Id` — a fresh per-build GUID, logged in
`App.xaml.cs:32` as `Build {Id} (compiled {BuiltLocal})`) as return data on the command line.
NOT yet implemented — queued, do not fold into another slice.
- **TASK 37** — defaults/current layout split (see above + `TASKS/task-37-defaults-current-split.md`).
- **Webcam take fix 2 of 2:** native WMF death when `MediaCapture.Failed` fires mid-stream and
`SafeStopAsync` disposes reader+capture while the frame thread sits in `TryAcquireLatestFrame`. No
repro/test — deferred per spin-guard.
- **Multi-camera selector slice (user-queued):** ≥2 webcams → web-cam selection in the App Settings
dialog (gear). Currently ≥2 cameras → nothing shows in settings.
- **Static (+) catalog slice (user-queued):** all catalog rows always listed, greyed with named
reason tooltips.
- **Device re-verify (one take session):** slice-18 cache verdicts; webcam fresh-launch with the
browser holding the camera → red alert that clears after Retry once the browser closes; Live must
OFFER Web Cam while Chat holds it.
- **No push yet** — after the take verdict, decide push with the user.
## Open threads (carried)
- Audio-silence verification — fixed (`724af14`); creator heard real audio.
- Webcam MJPG missing / ~10–14Hz, truncation-with-dynamic-scenes — queued.
- Sync control user-doc tutorial — REQUIRED before 1.0 (creator directive; TASK 22).
- Signed A/V sync: verify the negative (advance) direction on device.
- The 2026-09-18 "reorder doesn't save" stale-binary theory — RETRACTED 2026-09-20. The builder was
right; the cross-table z-order bug above is the real mechanism. Never re-air the stale-binary claim.
## Landmines
- testhost shares startup.log — filter by time; the app also writes fake-device failures
('test-camera', 'dev-physical-9') from CameraManager unit tests.
- `cmd.exe /c "taskkill /F /IM ytLive.exe"` (WSL double-slashes mangle) before rebuilds — a live
app process locks `ytLive.exe` (MSB3021).
- Build/tests: **Windows dotnet host** (`/mnt/c/Program Files/dotnet/dotnet.exe`). 0 warnings —
only `./scripts/verify.sh "<files>"`'s clean build counts. Running `ytLive.csproj` alone does NOT
rebuild `ytLive.Tests.dll` — run the Tests csproj before `vstest`. Known audio flake:
`Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` — stale loopback sample in the pipe when the
mute flips. Unrelated; do not "fix" inside another slice.
- **Assert footguns in this repo's xUnit:** `Assert.Null(x, "msg")`/`Assert.Single(coll, "msg")` do
NOT take a message — the 2-arg overloads mean something else (predicate/item). Empty message = 1 arg.
- The webcam's real state lives in `%APPDATA%\ytLlive\ytLlive.db` (`layout.db` there is a 0-byte
legacy file). sqlite3 at `/home/gramps/android-sdk/platform-tools/sqlite3`.
- FramePump tests asserting per-frame CONTENT need a STABLE scene; cache-sensitive assertions can't
use a call-count resolver flip (tick resolves twice) — key alternation on `OutputIndex`.
- `%APPDATA%\ytLive\ytLive.db` = real layout DB (`layout.db` is a 0-byte legacy file). sqlite3 at
`/root/android-sdk/platform-tools/sqlite3` (or the SDK copy) for DB checks.
- ffmpeg/ffprobe: `/mnt/c/Program Files/Krita (x64)/bin/` with Windows paths.
- `MyMistakes.md`: WINRT resource-allocation RECIPE, freeze-audit RECIPE, A/V sync recipe, deadline
pacing, CoreMessaging DQ, WGC-CLIP + slice blocks. Grep before re-deriving.
- **Test-host crash (fixed):** GC'd `WinEventProc` — `Shutdown()` unhooks the foreground-event hook;
never re-introduce `StartWatching` without matching teardown; dialogs with root-relative `/Assets/…`
Icon can't be instantiated by tests (use `/ytLive;component/…`).
- **Scene.Elements layer tests:** any seeded scene with `HasBackground=1` loads a healed
`Background` element pinned at `Elements[0]` (created by `EnsureBackground` on `StagedScene` set) —
compute expected layer orders from the live collection, never hardcode indices.
- **Cross-type element order tests** must assert through a FRESH `LayoutStore` (the restart), not the
running app's store — that's the read-back path the builder's bug exercised.
- `C:\tmpout` is for ffmpeg evidence artifacts; keep them out of the repo.
## Next step
The ordering fix is the builder-facing half of the "webcam between sources" bug — a manual take
(relaunch, live view, drag Web Resource-0 below the webcam, restart, expect it pinned there) closes
it. Then the copy with the builder on any remaining repro, then decide push. TASK 37 (defaults vs
current) and the build-id-on-exit demand are queued for a later slice — do not fold them in here.
- `ai.md` §Local recording (TASK 18, ≈lines 605-640) = the authoritative pill/state-model doc
(record-OR-live, armable signed-out, Start-button-face).