Bind broadcasts explicitly; the week-long RTMP dead end was never a bind
root cause (proven, not inferred): contentDetails.boundStreamId on liveBroadcasts.insert does NOT bind the broadcast. Our diagnostic reads lifeCycle=created privacy=private autoStart=true bound=NONE boundMatches=NO-BINDING | streamStatus=active created is Google's "incomplete settings" state, so enableAutoStart has no bound stream to fire from; close-out then 403s invalidTransition because created -> complete is not a legal edge. A reusable stream accepts bytes for a broadcast that isn't watching it, so a perfectly healthy 99MB RTMP session is exactly what an unbound broadcast looks like -- every observable pointed away from the binding. Google's migration guide states the rule outright: a liveBroadcast must be bound to exactly one stream before it can start, and is not bound at creation. That was in the docs read during the original design and was not applied. Shipped: - BindBroadcastAsync -> POST liveBroadcasts/bind?part=id&id&streamId (no body), called from PrepareAndStartLiveAsync right after CreateBroadcast, before the pump. Failure is fatal to the session: an unbound broadcast can never go live, so a live-looking UI over one is a lie. - boundStreamId removed from the insert body, and the streamId parameter removed from CreateBroadcast. A parameter that looks meaningful but cannot affect the outcome is what made the misdiagnosis possible. - Replaced CreateBroadcast_With_StreamId_Binds_Reusable_Stream_At_Insert, which asserted the disproven behaviour and therefore laundered a guess into a guarantee, with tests for the omitted field and the real bind endpoint. Error messages (creator ruling 2026-10-04): bind failures are not all "check the sign-in". Six documented reasons exist and only one is auth; BindFailureMessage gives each its own text, and the fallback passes the raw reason through verbatim so an unmapped reason stays greppable. liveBroadcastBindingNotAllowed matters most -- it means our ordering is wrong, and a generic re-auth message would hide a bug in our own code behind a plausible excuse. See MyMistakes.md. ai.md: also corrects the TASK 44 claim that liveChatId is only populated once the broadcast is live. The diagnostic shows it present while lifecycle is created. That claim was inferred from a sample's active-status guard, which is not a spec. Suite 395/395, 0 warnings.
This commit is contained in:
@@ -1399,3 +1399,42 @@ Corollary, and the part I keep skipping: **a repeated user correction is a docum
|
||||
not a user annoyance.** The second time the same question gets asked, the correct response is not a
|
||||
better answer — it is to go find why the written answer did not land, and fix *that*. Answering
|
||||
well the first time was never the problem; the map was not carrying the instruction.
|
||||
|
||||
---
|
||||
|
||||
## Error messages: write for the maintenance dev, not for the toast (creator ruling 2026-10-04)
|
||||
|
||||
The creator's standard, verbatim: *"my advice for error messages is to give some thought to the
|
||||
poor maintenance dev who's got to slog through this code tracking down bugs... generic error
|
||||
messages or, worse, error consolidation would ruin their day completely."* Extended on the ANSI
|
||||
point: *"didn't matter how cool it looked if you didn't understand what it was trying to tell you."*
|
||||
|
||||
**The rule: legibility outranks polish, and a cause must never be collapsed into a bucket.**
|
||||
|
||||
1. **Never consolidate distinct causes.** The first bind-failure implementation mapped seven
|
||||
documented reasons to seven messages and then funnelled everything unmapped into
|
||||
`_ => $"YouTube rejected the stream binding ({status})."` — which is the trap wearing a
|
||||
friendly face. YouTube adds an eighth reason, the generic branch swallows it, and the dev
|
||||
greps for a string the API never emits. **An unmapped reason is passed through verbatim so it
|
||||
stays greppable against the raw response.** A friendly sentence may be *added* to an unknown
|
||||
cause; it may never *replace* it.
|
||||
2. **Never let the toast be the only record.** The user-facing string may be reworded; the log
|
||||
must keep status + reason + the whole response body verbatim. Toast = friendly, log = exact.
|
||||
3. **"Re-authenticate" is not a universal answer.** It was my first instinct for a bind failure and
|
||||
it is correct for exactly one of six documented reasons. Wrong for the other five it actively
|
||||
misdirects — worst for `liveBroadcastBindingNotAllowed`, which means *our own ordering* is
|
||||
broken. Sending the dev to re-authenticate hides a bug in our own code behind a plausible
|
||||
excuse. **Before writing a fix-it suggestion, ask which of the documented reasons this text
|
||||
would be wrong for.**
|
||||
4. **A sentinel that means three things is not a sentinel.** The diagnostic emitted `?` for
|
||||
"field absent", "field unparseable" and "endpoint returned something else" — an ANSI-bright
|
||||
`?` that looked informative and told the reader nothing. Distinct states get distinct tokens
|
||||
(`unset` vs `unread` vs a real value), and unreadable input is never reported as a finding.
|
||||
5. **Check the docs' failure modes, not just the happy path** (this one already bit a week of
|
||||
diagnosis — see the SOLVED box in `ai.md`). Every reason the API can refuse must have its own
|
||||
handling and its own test, or the refusal is invisible.
|
||||
|
||||
**Corollary, same shape as the decision-note lesson above:** the test suite is part of this
|
||||
standard. A test that asserts a *disproven* behaviour doesn't just fail to help — it launders a
|
||||
guess into a guarantee and makes the next reader stop looking. If a test's expectation is wrong,
|
||||
the expectation is the bug; fix the belief, don't delete the assertion.
|
||||
|
||||
Reference in New Issue
Block a user