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:
2026-10-04 15:27:01 -07:00
parent 8fbc3701d2
commit 07a088a23f
5 changed files with 285 additions and 24 deletions
+39
View File
@@ -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.