Skip to content

refactor(orchestration): PR-5 extract HandoffCeremonyProducer (T532–T533, gh-#772) - #824

Merged
genwave-radio merged 2 commits into
mainfrom
orchestration/handoff-producer-457
Sep 20, 2026
Merged

genwave-radio merged 2 commits into
mainfrom
orchestration/handoff-producer-457

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

🎛️ PR-5 — the handoff ceremony gets its own producer (T532–T533)

Fifth PR of the orchestrator split (gh-#401), covering STORY-457. Follows #805 (PR-1), #808 (PR-2), #818 (PR-3), #820 (PR-4) and the v5.10.0 pins (#821).

🎯 What changes

The show-boundary handoff — arming a SignOff before a boundary, holding the SignOn until the queued tail clears, re-arming when a unit straddles — moved off the Orchestrator into its own HandoffCeremonyProducer. It was the last large block of boundary state living on a class that had already handed off planning (PR-3) and the speaker snapshot (PR-4).

Task Change
T532 Extract HandoffCeremonyProducer (ArmAsync, CaptureCrossingTrack, HoldSignOnPastQueuedTail, warn-once); Orchestrator keeps SignOffLeadTime; dedupe matrix facts
T533 Wire: a dev-station cycle across real show boundaries, booth_log ceremony rows against main

Orchestrator.cs goes 1699 → 1169 lines. SignOffLeadTime deliberately stays behind (F190.4) because BoundaryCadenceCovenantPostConfigure.cs:57 consumes it. Story457's 13 facts come off pending: T532; FactMethodCount = 559 is unmoved — no new [Fact], only un-skips.

✅ Gate

Full solution, Release, warnings as errors:

Core 185 · Context 78 · Orchestration 536/29sk · Ads 222 · Plugins 53
MediaLibrary 169 · Tts 889/2sk · Host 3077/13sk · Architecture 174/9sk
0 failed · 0 warnings

Orchestration moves +13/−13 off the 523/42 baseline — exactly the un-skips, nothing else.

Smoke: the production Host binary booted against a fixture Postgres and answered GET /health 200 Healthy with PlayoutFeederService constructed and ticking. The feeder's construction is what drags INextItemProviderOrchestrator → the new required HandoffCeremonyProducer argument through the real composition root, so a mis-wired constructor fails the smoke rather than passing it.

📻 T533 — the wire evidence

Ran on the dev station, not a fixture: same box, same Postgres, same Liquidsoap/Icecast/Kokoro, same mistral:7b on the LLM host. Only the api image was swapped between e932b27 (main, this branch's parent — so the A/B delta is exactly T532) and 282160a (branch). Boundaries came from real station.segment_schedule rows; every row below is station.booth_log.

patter-aired is a render event and track-started with a populated segment_kind is the air event (BoothLogWriter.cs:67-70), so acceptance is counted on the latter only.

Boundary Image Direction SignOff aired SignOn aired
12:30 main e932b27 Nova Q → Default ✅ 12:33:47 ✅ 12:39:41
17:30 branch 282160a Nova Q → Default 17:39:28 17:39:55
18:00 branch 282160a Default → Nova Q 18:01:22 18:07:37

Exactly one SignOff and one SignOn per boundary, as main. The aired-kind profile over the branch window also matches main's — BackAnnounce 11, LeadIn 10, music 9, SignOff 2, SignOn 2, Crosstalk 1, StationId 1.

The 18:00 boundary is the useful one: it logged outcome=declined (floor=90s) rung=Straddle and still aired both pieces. A straddle re-arms the held SignOn rather than dropping it, and the extracted producer does that as the Orchestrator did.

⚠️ A detour worth recording

The first branch boundaries were run against a local LLM stub, because the real LLM host was down. Under the stub the branch aired zero BackAnnounce over 60 units and no ceremonies, which read as a regression. It was not: re-running main under the same stub also aired zero BackAnnounce, and the profile returned to normal the moment a real LLM was back. Stub copy is one line, so pieces render but are not genuinely LLM-authored, and SPEC F92.4/F92.5 has PatterTemplateRenderer drop exactly those. A stubbed LLM cannot be used to judge break or ceremony delivery — it silently changes what airs.

One residue is unexplained: under the stub, main aired a SignOff at its 17:00 boundary where the branch aired none across three. It did not reproduce under real conditions on either image, and acceptance is defined against main's real-LLM behaviour, so it was not chased further. Noted rather than buried.

🧹 Reviewer observations carried, not blocking

One opus round, PASS-WITH-NOTES. The three notes were comment/doc-only and are fixed in 282160a: a miscited SPEC clause at the step-2.5 call site, a stale-method-name sweep across 5 production and 17 spec comment sites, and two restored rationale paragraphs on HoldSignOnPastQueuedTail explaining why a separate gate beats re-stamping Due.

Three observations are deliberately not actioned here, to keep the diff extraction-only:

  • O1 — collapse the duplicated held/heldNotBefore peek and the F189.5 speaker-reuse ternary (HandoffCeremonyProducer.cs:344-354, :426-433) into a ReArmSignOn(...) helper.
  • O2 — a shared boundary-cadence constants holder, to retire the static back-reference to Orchestrator.SignOffLeadTime.
  • O3Story460_TheOrchestratorAfter (all pending: T537) owns the pin that closes the target-typed return new(...) evasion the six extra spec files rode. That lands in PR-7.

📦 Next

T534BreakRenderer.RenderAsync(plan, ct) replaces the T522 interim render loop on branch orchestration/break-renderer-458.

PR-5 of the orchestrator split. The handoff ceremony — arming the sign-off /
sign-on pair at a show boundary, capturing the track that crosses it, and
holding a sign-on past an over-long queued tail — moves out of Orchestrator
into its own collaborator, GenWave.Orchestration.HandoffCeremonyProducer.

What moved:
  - Orchestrator.EnqueueHandoffCeremonyAsync    -> HandoffCeremonyProducer.ArmAsync
  - Orchestrator.CaptureCrossingTrackForHeldSignOn
                                                -> HandoffCeremonyProducer.CaptureCrossingTrack
  - the queued-tail sign-on hold                -> HandoffCeremonyProducer.HoldSignOnPastQueuedTail

Orchestrator drops from 1699 to 1169 lines and now calls the producer at
step 2.5 rather than performing the ceremony itself. SPEC F190.1 gives ArmAsync
an explicit `now`, so the call site passes the same instant it already read for
BreakContext -- the drain and the arm reason from one clock read instead of two
a unit apart. The arm-once and warn-once semantics are unchanged: the producer
is registered as a singleton alongside the INextItemProvider that holds it.

Story457's 13 facts come off `pending: T532` and assert the extracted seam
directly. The six other spec files and OrchestratorBuilder change only to pass
the new constructor argument -- they hand-build Orchestrator via target-typed
`return new(...)`, which is the evasion Story460 will pin at T537. Comment
references to the old method names are swept across production and specs.

Gate: full solution green -- 0 warnings, 0 failed, Orchestration 536 passed /
29 skipped (+13/-13 from the 523/42 baseline), every other suite unmoved.
Smoke: the production Host binary boots against a fixture Postgres and answers
GET /health 200 Healthy, with PlayoutFeederService constructed and ticking --
which is what resolves Orchestrator's new required argument through the real
composition root.
@genwave-radio
genwave-radio merged commit c1a31b9 into main Sep 20, 2026
11 checks passed
@genwave-radio
genwave-radio deleted the orchestration/handoff-producer-457 branch September 20, 2026 00:31
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant