refactor(orchestration): PR-5 extract HandoffCeremonyProducer (T532–T533, gh-#772) - #824
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎛️ 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).HandoffCeremonyProducer(ArmAsync,CaptureCrossingTrack,HoldSignOnPastQueuedTail, warn-once); Orchestrator keepsSignOffLeadTime; dedupe matrix factsbooth_logceremony rows against mainOrchestrator.csgoes 1699 → 1169 lines.SignOffLeadTimedeliberately stays behind (F190.4) becauseBoundaryCadenceCovenantPostConfigure.cs:57consumes it. Story457's 13 facts come offpending: T532;FactMethodCount = 559is unmoved — no new[Fact], only un-skips.✅ Gate
Full solution, Release, warnings as errors:
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 /health200 Healthy withPlayoutFeederServiceconstructed and ticking. The feeder's construction is what dragsINextItemProvider→Orchestrator→ the new requiredHandoffCeremonyProducerargument 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:7bon the LLM host. Only the api image was swapped betweene932b27(main, this branch's parent — so the A/B delta is exactly T532) and282160a(branch). Boundaries came from realstation.segment_schedulerows; every row below isstation.booth_log.patter-airedis a render event andtrack-startedwith a populatedsegment_kindis the air event (BoothLogWriter.cs:67-70), so acceptance is counted on the latter only.e932b27282160a282160aExactly 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=Straddleand 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.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
PatterTemplateRendererdrop 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 in282160a: 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 onHoldSignOnPastQueuedTailexplaining why a separate gate beats re-stampingDue.Three observations are deliberately not actioned here, to keep the diff extraction-only:
held/heldNotBeforepeek and the F189.5 speaker-reuse ternary (HandoffCeremonyProducer.cs:344-354,:426-433) into aReArmSignOn(...)helper.Orchestrator.SignOffLeadTime.Story460_TheOrchestratorAfter(allpending: T537) owns the pin that closes the target-typedreturn new(...)evasion the six extra spec files rode. That lands in PR-7.📦 Next
T534 —
BreakRenderer.RenderAsync(plan, ct)replaces the T522 interim render loop on branchorchestration/break-renderer-458.