PR-7: BreakDelivery + the Orchestrator's fitness pins (T536–T538, gh-#401) - #826
Merged
Merged
Conversation
Moves the deliver phase out of the Orchestrator into its own class. The
Orchestrator's render path is now three statements: render, check the token,
enqueue. BreakDelivery owns stamping, drop reporting, and settling every
reservation (SPEC F192).
New types:
BreakDelivery - Deliver(plan, outcomes) -> BreakOutcome, plus
Abandon(plan) for the record-only path (F192.4).
BreakOutcome - Items + Reservations.
ReservationOutcome - closed hierarchy: Aired / Dropped / Abandoned.
WarnDropSubject - closed hierarchy: Announcement / ContextProvider.
The subject hierarchy replaces PlannedSlot.ContextProviderKey: a WarnDrop now
carries what it warns about instead of the slot carrying a nullable field that
only two slot kinds ever set. WarnDrop.Subject is required - a subject-less
WarnDrop no longer constructs, so the crash path it used to open is gone with
the dead DropPolicyFor arm that minted it.
The cancellation check sits between render and delivery, not before render.
Every ReadySource slot is a Task.FromResult, so a pre-render check alone let a
completed slot's outcome reach the buffer regardless of when the feeder's token
fired at shutdown - exactly the partial enqueue F192.5 forbids.
AnnouncementIdOf moves here from BreakRenderer: one reader of the announcement
id, not two. BreakDelivery's seams are required constructor parameters, not
optional ones - the DI call site supplies the NoOp fallbacks.
Orchestrator.cs 1138 -> 1074 lines; Orchestrator.Render.cs 109 -> 43.
Story459 goes 11 -> 13 facts; solution fact count 559 -> 561.
Smoked on dev: image rebuilt, api recreated, /health 200 Healthy (DI resolves
BreakDelivery now its seams are required - unit tests build it by hand). Nine
minute soak: 3 AIR events, 0 drop lines, no UnreachableException and no
ArgumentException from the new length guard, 7 TTS renders all success.
…401) STORY-460 / SPEC F193. Nine facts in Architecture.Tests pin the split Orchestrator so it cannot silently regrow, plus one dead-code deletion. AC1 reflects every public constructor in GenWave.Orchestration for an optional interface parameter (SPEC F193.2). The rule is false today for 10 classes, so the fact asserts set equality against a named, dated 25-entry baseline in Support/OrchestrationConstructorSeams.cs. Set equality, not Except: a new violation fails, and so does a fixed one whose baseline entry is not struck in the same change. The baseline is local rather than wired through ExemptionBaseline because Story290_DependencyLaws requires every ArchitectureExemption.LawId be a member of LawId.All, and this rule is not a numbered law. AC2 lifts Story451's `new Orchestrator(` text scan into Support/ConstructorCallScan.cs so both specs read one cached scan instead of two copies that could drift. The split literal is preserved so the scan does not self-match. AC3 reflects typeof(Orchestrator) for INextItemProvider, IBoundaryFitLog, SignOffLeadTime and TimeDateHonestyThreshold. AC4 drives a live ceremony-only unit through OrchestratorBuilder rather than text-matching the frozen replay table, whose O9 trace is byte-identical to O5's. Cadence asks for a lead-in on every unit, so the absence of one proves the decline in TryServeCeremonyOnlyUnitAsync actually fired; a third fact asserts the plan is a single slot (SPEC F186.2(e)). Neutering the decline path turns both red. AC5 asserts SPEC F193.5's narrow claim — GenWave.Orchestration references only Core and Abstractions — in place of a vacuous suite-is-green fact. Deletes Orchestrator's `events` parameter and field, dead since T536 moved event publishing to BreakDelivery: the field initializer was the only reader of the parameter. Removes the nine now-invalid `events:` arguments across the DI root, the builder, and seven spec helpers. BreakDelivery remains the sole IStationEventSink consumer on the GetNextAsync path. Orchestrator.cs 1066 + Orchestrator.Render.cs 43 = 1109 lines, under half the 2687-line 2026-09-17 baseline. Full solution green: Architecture 184/0, Host 3077/13sk, Orchestration 564/3sk, 0 warnings. Smoke: production image rebuilt, api recreated, GET /health 200 Healthy, 3 AIR rows (LeadIn, BackAnnounce, LeadIn), 10/10 renders successful, zero drops.
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-7 — the delivery phase gets its own collaborator, and the Orchestrator gets its fitness pins (T536–T538)
Seventh and final code PR of the orchestrator split (gh-#401), covering STORY-459 and STORY-460. Follows #805 (PR-1), #808 (PR-2), #818 (PR-3), #820 (PR-4), the v5.10.0 pins (#821) and #825 (PR-6).
🎯 What changes
The last of the three phases comes off the Orchestrator. Turning a
BreakPlanplus the renderer'sSlotOutcomelist into stamped items, drop reports and settled reservations now lives inBreakDelivery; the Orchestrator's render file is down to the three calls and an enqueue.BreakDelivery.Deliver(plan, outcomes) → BreakOutcome; Story459's facts come offpending: T536Orchestrator.eventsas deadOrchestrator.csgoes 1138 → 1066 lines;Orchestrator.Render.cs109 → 43. Against the 2687-line Orchestrator this epic started from on 09-17, that is 1109 lines across both files — well under half.🧾 Delivery is now a pure function of (plan, outcomes)
No buffer, no clock, no TTS backend, no Orchestrator. A spec drives it with a hand-built plan and an outcome list, which is what makes the stamp rules and the drop-policy rules testable at all (F192.6 / AC6).
Three things settle in the walk, in ordinal order:
AnnouncementMediaId.Wrapon announcements, crosstalktts:crosstalk:{file}ids (F192.1).DropPolicywith today's text;WarnAndEventalso publishesHandoffPieceDroppedthrough the event sink (F192.2).AiredorDropped(reason). Nothing is released; the outcome is on the record so a later story can act on it (F192.4).Cancellation is a named deviation from today's behaviour, not an accident: when
ctfires before delivery, no item of the unit reaches the buffer and every reservation isAbandoned(F192.5). Today's code partially enqueues. It is observable only at shutdown, and one fact pins it.🔒 The Wrap has one home now
T534 left
AnnouncementMediaId.WraponBreakRendererwhile F192.1 assigns it to delivery, so the helper was byte-duplicated across two files. It now sits only inBreakDelivery, next to the DJ-name stamp it always lived beside on the Orchestrator — one class owns every post-render stamp.💥 The length guard (T534 review carry-forward)
RenderPlanAsyncpairedoutcomes[i]withplan.Slots[i]with nothing enforcing equal length. A silent mis-pair means a wrong announcement id in a drop WARN and a wrong DJ-name stamp, soDelivernow throwsArgumentExceptionon a mismatch instead.📐 The fitness pins (T537, STORY-460)
Story460_TheOrchestratorAfteradds 10 facts to Architecture.Tests (183 → 184 total, 0 skips):GenWave.Orchestrationdeclares an optional interface parameter — reflection over every public ctor, against a named dated baselinenew Orchestrator(occurs only inOrchestratorBuilderandAddGenWaveOrchestration(text scan oversrc/+tests/)INextItemProvider,IBoundaryFitLog,SignOffLeadTime, theFitToleranceconstants andTimeDateHonestyThresholdGenWave.Orchestrationreferences only Core + AbstractionsRound 1 failed because two of the nine facts could not go red. AC4's pair passed with
TryServeCeremonyOnlyUnitAsyncneutered —ListsNoLeadInwas guaranteed by its own arrange (LeadInBeforeEachTrack = false), not by the ceremony. Round 2 flipped lead-in on and addedDrawsNothingFromTheCatalog; under the same sandbox probe those two now go red.ListsTheSignOffis still insensitive — the ordinary music path drains the same queued SignOff — so the single-slot fact is the load-bearing one, and its docstring says so rather than overclaiming.🗑️
Orchestrator.eventsdeletedDead since T536 moved publishing to
BreakDelivery: the field initializer was the parameter's only reader (CS9113: Parameter 'events' is unread). Nineevents:arguments came out — the DI root,OrchestratorBuilder, and 7 spec helpers. Each spec edit is exactly one forced deletion of an argument to a parameter that no longer exists.These are disclosed, not fixed — each needs a
/designruling, and none is fixable inside a fitness-pin task. They are carried to the landing PR.MusicSelectionPolicy,ScheduleResolver,RollingPatterDurationEstimator,CachingScheduleResolver,BreakRenderer,PersonaRanker,HandoffCeremonyProducer,ClockAnchoredImagingProducer,BreakPlannerandOrchestrator. AC1 therefore ships green against a named, dated 25-entry baseline (Support/OrchestrationConstructorSeams.cs), asserted as set equality so the list can only shrink deliberately. Burning it down means ~200 call sites in projects this PR does not own. F193.2 must not be ticked clean.TimeDateHonestyThreshold, which has been dead since T527 moved classification intoBreakPlanner— the live duplicate isBreakPlanner.cs:71, read atBreakPlanner.Slots.cs:216. The constant is kept only because the SPEC names it, with a docstring that says so. Deleting it would make a spec clause false by fiat; that is the SPEC owner's call.return new(…), which a scan for the literalnew Orchestrator(is structurally blind to. AC2's "two construction sites" story is untrue as written. Closing it means broadening past the spec's letter.Also:
ExposesTimeDateHonestyThresholdreflects a private field, so "exposes" tracks AC3's wording rather than reality — worth fixing alongside (2).🔌 Wire evidence (T538)
Two measured hours on the dev station, identical shape each side — force-recreate
api, wait for/health200, 5 minutes of unmeasured warmup, then 60 minutes counted. Only theapiimage changed between arms, so the A/B delta is exactly T536 + T537. Real LLM throughout (mistral:7bonrunes,Llm:DegradationPin: auto), which is what makes a delivery A/B mean anything.booth_log kind histogram — the acceptance:
98c9a23a8feb2btrack-startedpatter-airedhandoff-droppedThe kind set is identical: exactly those two kinds on both sides, no
handoff-dropped, nomode-changed, noannouncement-aired.Aired profile (
track-startedwith a populatedsegment_kind— the AIR event;patter-airedis the RENDER event):Same set, different counts — and the counts are library shuffle, not throughput. Main's tracks ran ~11% longer (avg 292 s vs the branch's 263 s), so main fit fewer unit boundaries into its hour and therefore rendered less patter. Raw music minutes (57.0 branch / 53.6 main) carry a ±1-track edge effect worth ~4–5 min, so the edge-free statistic is the one that settles it:
Identical per-unit shape. That is the number that says delivery is neutral.
Drops — all three warning families plus the event rows:
Handoff piece … droppedContext segment for provider … droppedAnnouncement … droppedhandoff-droppedbooth_log rowsA zero-vs-zero drop result is weak evidence and should be read as such. Two quiet hours prove no new drops; they do not exercise the drop path at all. The drop path's real coverage is Story459's sad-path facts on the fake clock, not this hour.
Renders: 100%
outcome=successon both sides — branch 23/23 (10 LeadIn, 10 BackAnnounce, 3 StationId), main 18/18 (8/8/2). Render counts are offset from air counts by the ~10-minute queued-ahead buffer, so the two are not a matched pair; the signal here is that neither side logged a single render failure.Faults: 0
Unhandled exception/UnreachableException/[FTL]/System.ArgumentExceptionlines on either side — the last of those being the new length guard, which stayed silent.✅ Verification
98c9a23:dotnet build -c Release -warnaserror→ 0 warnings / 0 errors; full solution → 0 failed. Core 185 · Context 78 · Ads 222 · Orchestration 564/3sk · Plugins 53 · MediaLibrary 169 · Tts 889/2sk · Architecture 184/0sk · Host 3077/13sk.Story452_BreakCharacterisationReplay.csandStory452_PinnedTables.cscarry no diff in this PR, which is the whole point of having written them before the first move.apirecreated,GET /health→ 200 Healthy through the deployed container, then a soak polled until AIR rows appeared (track-startedwith a populatedsegment_kind) — 3 each time, zero drops, noUnreachableException, noArgumentExceptionout of the new length guard.📋 Not in this PR
The landing PR (T539) carries the
compose.pinned+ CHANGELOG pins, closes the ARCHITECTURE TODO, and is where the three F193 drifts above get their ruling. Still open from earlier rounds, all non-blocking:BreakDelivery.DeliverSlotruns 34 lines;Story454's sharedUnusedWarnSubject; an 8-line rationale comment copy-pasted into 6 spec helpers;BreakDelivery.Abandonis public with one test caller and no production caller (record-only per F192.4); aReArmSignOn(…)helper; a shared boundary-cadence constants holder.🚫 No merge without Dean.