Skip to content

PR-7: BreakDelivery + the Orchestrator's fitness pins (T536–T538, gh-#401) - #826

Merged
genwave-radio merged 2 commits into
mainfrom
orchestration/break-delivery-459
Sep 20, 2026
Merged

genwave-radio merged 2 commits into
mainfrom
orchestration/break-delivery-459

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

🎚️ 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 BreakPlan plus the renderer's SlotOutcome list into stamped items, drop reports and settled reservations now lives in BreakDelivery; the Orchestrator's render file is down to the three calls and an enqueue.

Task Change
T536 Extract BreakDelivery.Deliver(plan, outcomes) → BreakOutcome; Story459's facts come off pending: T536
T537 Fitness pins for the Orchestrator after (Story460); delete Orchestrator.events as dead
T538 Wire: dev-station A/B, booth_log kind histogram + drop count against main

Orchestrator.cs goes 1138 → 1066 lines; Orchestrator.Render.cs 109 → 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)

public sealed class BreakDelivery(
    ILogger<BreakDelivery> logger,
    IStationEventSink events,
    IPatterDurationEstimator patterEstimator)
{
    public BreakOutcome Deliver(BreakPlan plan, IReadOnlyList<SlotOutcome> 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:

  • Items — the rendered survivors, stamped exactly as before: DJ-name attribution on StationId / Announcement / Ad, AnnouncementMediaId.Wrap on announcements, crosstalk tts:crosstalk:{file} ids (F192.1).
  • Drops — reported per the slot's own DropPolicy with today's text; WarnAndEvent also publishes HandoffPieceDropped through the event sink (F192.2).
  • Reservations — every reservation the plan claimed gets exactly one outcome, Aired or Dropped(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 ct fires before delivery, no item of the unit reaches the buffer and every reservation is Abandoned (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.Wrap on BreakRenderer while F192.1 assigns it to delivery, so the helper was byte-duplicated across two files. It now sits only in BreakDelivery, 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)

RenderPlanAsync paired outcomes[i] with plan.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, so Deliver now throws ArgumentException on a mismatch instead.

📐 The fitness pins (T537, STORY-460)

Story460_TheOrchestratorAfter adds 10 facts to Architecture.Tests (183 → 184 total, 0 skips):

AC Pin
AC1 No public constructor in GenWave.Orchestration declares an optional interface parameter — reflection over every public ctor, against a named dated baseline
AC2 new Orchestrator( occurs only in OrchestratorBuilder and AddGenWaveOrchestration (text scan over src/ + tests/)
AC3 The Orchestrator still exposes INextItemProvider, IBoundaryFitLog, SignOffLeadTime, the FitTolerance constants and TimeDateHonestyThreshold
AC4 A ceremony-only unit goes through plan → render → deliver: the plan trace lists the SignOff, lists no lead-in, and draws nothing from the catalog
AC5 GenWave.Orchestration references only Core + Abstractions

Round 1 failed because two of the nine facts could not go red. AC4's pair passed with TryServeCeremonyOnlyUnitAsync neutered — ListsNoLeadIn was guaranteed by its own arrange (LeadInBeforeEachTrack = false), not by the ceremony. Round 2 flipped lead-in on and added DrawsNothingFromTheCatalog; under the same sandbox probe those two now go red. ListsTheSignOff is 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.events deleted

Dead since T536 moved publishing to BreakDelivery: the field initializer was the parameter's only reader (CS9113: Parameter 'events' is unread). Nine events: 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.

⚠️ Three ways SPEC F193 has drifted from the tree

These are disclosed, not fixed — each needs a /design ruling, and none is fixable inside a fitness-pin task. They are carried to the landing PR.

  1. F193.2 is false today for 10 classes / 25 parameters, not just the Orchestrator: MusicSelectionPolicy, ScheduleResolver, RollingPatterDurationEstimator, CachingScheduleResolver, BreakRenderer, PersonaRanker, HandoffCeremonyProducer, ClockAnchoredImagingProducer, BreakPlanner and Orchestrator. 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.
  2. F193.3 names TimeDateHonestyThreshold, which has been dead since T527 moved classification into BreakPlanner — the live duplicate is BreakPlanner.cs:71, read at BreakPlanner.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.
  3. F193.4's text scan sees 2 of 9 construction sites. Seven specs build the Orchestrator with target-typed return new(…), which a scan for the literal new 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: ExposesTimeDateHonestyThreshold reflects 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 /health 200, 5 minutes of unmeasured warmup, then 60 minutes counted. Only the api image changed between arms, so the A/B delta is exactly T536 + T537. Real LLM throughout (mistral:7b on runes, Llm:DegradationPin: auto), which is what makes a delivery A/B mean anything.

booth_log kind histogram — the acceptance:

kind branch 98c9a23 main a8feb2b
track-started 39 33
patter-aired 23 18
handoff-dropped 0 0

The kind set is identical: exactly those two kinds on both sides, no handoff-dropped, no mode-changed, no announcement-aired.

Aired profile (track-started with a populated segment_kind — the AIR event; patter-aired is the RENDER event):

aired branch main
BackAnnounce 12 10
LeadIn 12 11
StationId 2 1
(music) 13 11

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:

Aired patter pieces per music track: branch 2.00, main 2.00.

Identical per-unit shape. That is the number that says delivery is neutral.

Drops — all three warning families plus the event rows:

branch main
Handoff piece … dropped 0 0
Context segment for provider … dropped 0 0
Announcement … dropped 0 0
handoff-dropped booth_log rows 0 0

A 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=success on 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.ArgumentException lines on either side — the last of those being the new length guard, which stayed silent.

✅ Verification

  • Gate on 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's characterisation replay is untouchedStory452_BreakCharacterisationReplay.cs and Story452_PinnedTables.cs carry no diff in this PR, which is the whole point of having written them before the first move.
  • Smoke (T536 and T537 each): image rebuilt on the dev station, api recreated, GET /health200 Healthy through the deployed container, then a soak polled until AIR rows appeared (track-started with a populated segment_kind) — 3 each time, zero drops, no UnreachableException, no ArgumentException out 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.DeliverSlot runs 34 lines; Story454's shared UnusedWarnSubject; an 8-line rationale comment copy-pasted into 6 spec helpers; BreakDelivery.Abandon is public with one test caller and no production caller (record-only per F192.4); a ReArmSignOn(…) helper; a shared boundary-cadence constants holder.

🚫 No merge without Dean.

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.
@genwave-radio
genwave-radio merged commit 8fb6f89 into main Sep 20, 2026
11 checks passed
@genwave-radio
genwave-radio deleted the orchestration/break-delivery-459 branch September 20, 2026 21:03
@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