Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions SEAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
> environment- or flag-gated `Add*` branch exists today), so nothing is known to be missing
> from this map for that reason.
>
> **79 seams across 6 projects.**
> **81 seams across 6 projects.**

## GenWave.Context (2 seams)

Expand All @@ -53,7 +53,7 @@
| `GenWave.Core.Abstractions.IContextPatterFactSource` | `GenWave.Context.ContextPipeline` | Singleton | also registered: `GenWave.Core.Abstractions.NoOpContextPatterFactSource` (GenWave.Core) |
| `GenWave.Core.Abstractions.IContextProvider` | `GenWave.Context.History.HistoryContextProvider` | Singleton | also registered: `GenWave.Context.Weather.WeatherContextProvider` (GenWave.Context) |

## GenWave.Host (21 seams)
## GenWave.Host (22 seams)

| Port | Adapter | Lifetime | Notes |
|---|---|---|---|
Expand All @@ -69,6 +69,7 @@
| `GenWave.Core.Abstractions.IRequestOverrideEnvelopeProvider` | `GenWave.Host.Options.OptionsMonitorRequestOverrideEnvelopeProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.IRotationSettingsProvider` | `GenWave.Host.Options.OptionsMonitorRotationSettingsProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.ISafeScopeProvider` | `GenWave.Host.Options.OptionsMonitorSafeScopeProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.IShowPatterCadenceProvider` | `GenWave.Host.Options.OptionsMonitorShowPatterCadenceProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.IStationClockProvider` | `GenWave.Host.Options.OptionsMonitorStationClockProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.IStationDefaultEnvelopeSource` | `GenWave.Host.Options.OptionsMonitorStationDefaultEnvelopeSource` | Singleton | β€” |
| `GenWave.Core.Abstractions.IStationEventSink` | `GenWave.Host.Playout.CompositeStationEventSink` | Singleton | also registered: `GenWave.Core.Events.NoOpStationEventSink` (GenWave.Abstractions) |
Expand Down Expand Up @@ -126,14 +127,15 @@
| `GenWave.Core.Abstractions.IThemeStore` | `GenWave.MediaLibrary.Station.ThemeRepository` | Singleton | β€” |
| `GenWave.Core.Abstractions.IYearLookup` | `GenWave.MediaLibrary.YearLookup.MusicBrainzYearLookup` | Singleton | β€” |

## GenWave.Orchestration (7 seams)
## GenWave.Orchestration (8 seams)

| Port | Adapter | Lifetime | Notes |
|---|---|---|---|
| `GenWave.Core.Abstractions.IActivePersonaAccessor` | `GenWave.Orchestration.OnAirPersonaAccessor` | Singleton | β€” |
| `GenWave.Core.Abstractions.IEnvelopeProvider` | `GenWave.Orchestration.ScheduleEnvelopeProvider` | Singleton | β€” |
| `GenWave.Core.Abstractions.INextItemProvider` | `GenWave.Orchestration.Orchestrator` | Singleton | β€” |
| `GenWave.Core.Abstractions.IPatterDurationEstimator` | `GenWave.Orchestration.RollingPatterDurationEstimator` | Singleton | β€” |
| `GenWave.Core.Abstractions.IShowFlavorLineSource` | `GenWave.Orchestration.ShowFlavorLineGate` | Singleton | β€” |
| `GenWave.Orchestration.IPersonaPickProvider` | `GenWave.Orchestration.RankerPersonaPickProvider` | Singleton | also registered: `GenWave.Orchestration.NoOpPersonaPickProvider` (GenWave.Orchestration) |
| `GenWave.Orchestration.IRandomSource` | `GenWave.Orchestration.SystemRandomSource` | Singleton | β€” |
| `GenWave.Orchestration.IRequestFulfillmentSource` | `GenWave.Orchestration.RequestFulfillmentProvider` | Singleton | also registered: `GenWave.Orchestration.NoOpRequestFulfillmentSource` (GenWave.Orchestration) |
Expand Down
6 changes: 6 additions & 0 deletions admin-ui/app/(authed)/settings/SettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ const FIELD_HELP_TEXT: Record<SettingsHelpKey, string> = {
"count. Off by default.",
"Station:Imaging:TimeAnnouncements":
"When on, a spoken time announcement airs at the top of every hour. Off by default.",

// ── Show-flavor patter line (SPEC F116.3) ───────────────────────────────────────────────────
"Station:Shows:PatterCadenceMinutes":
"How often a show's flavor may color an ordinary lead-in/back-announce, in minutes β€” shares " +
"the same one-line slot as the context patter lines above; a due context fact always wins. " +
"0 disables the show-flavor line. Accepted range: 0–1440.",
};

/**
Expand Down
1 change: 1 addition & 0 deletions admin-ui/app/(authed)/settings/settings-help-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const SETTINGS_HELP_KEYS = [
"Station:Location:SpokenName",
"Station:Imaging:ClockAnchoredIdents",
"Station:Imaging:TimeAnnouncements",
"Station:Shows:PatterCadenceMinutes",
] as const;

export type SettingsHelpKey = (typeof SETTINGS_HELP_KEYS)[number];
31 changes: 31 additions & 0 deletions src/GenWave.Abstractions/Abstractions/IMediaCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,37 @@ public interface IMediaCatalog
Task<MediaReference?> GetRandomReadyByImagingKindAsync(LibraryScope scope, ImagingKind kind, CancellationToken ct) =>
Task.FromResult<MediaReference?>(null);

/// <summary>
/// SPEC F117.1/F117.2 (STORY-309, PLAN T250) β€” <see cref="GetRandomReadyByImagingKindAsync(LibraryScope,ImagingKind,CancellationToken)"/>'s
/// show-scoped sibling: a genuinely NEW interface member, never that 3-arg member widened in
/// place. Widening the published 3-arg signature (even with a trailing optional parameter) would
/// have been a binary break for any already-compiled caller reaching for the OLD 3-arg overload β€”
/// a <see cref="System.MissingMethodException"/> at that call site, since the metadata signature
/// itself changes β€” and would have silently orphaned any pre-T250 implementer's own override: an
/// implementer that only ever overrode the 3-arg shape would, the moment a NEW caller reached for
/// a show-scoped call, have fallen through to a hardcoded-null default instead of the real (if
/// show-unaware) answer its own code already knows how to give.
/// <para>
/// Default-implemented (not abstract), same additive-contract discipline as every other DIM on
/// this interface β€” but this one's default body DELEGATES to the 3-arg member above (dropping
/// <paramref name="showId"/>) rather than fabricating <see langword="null"/>: a pre-T250
/// implementer that overrides ONLY the 3-arg shape still answers honestly β€” its own unscoped pool
/// β€” when reached through this NEW shape, rather than reporting a manufactured empty pool. The
/// ONE production implementer (<c>GenWave.MediaLibrary.Catalog.MediaRepository</c>) overrides BOTH
/// members explicitly; this default only ever matters for an implementer that has not opted in.
/// </para>
/// <para>
/// <paramref name="showId"/> preference ladder (see <c>MediaRepository</c>'s own concrete override
/// for the SQL): a row scoped to <paramref name="showId"/> is preferred, a station-wide
/// (<c>show_id</c> null) row is the fallback, and a row scoped to a DIFFERENT show is never a
/// candidate at all (F117.1, "scoped means scoped"). <see langword="null"/> means "no show" β€” the
/// F110.2-original behavior the 3-arg member above has always given.
/// </para>
/// </summary>
Task<MediaReference?> GetRandomReadyByImagingKindAsync(
LibraryScope scope, ImagingKind kind, long? showId, CancellationToken ct) =>
GetRandomReadyByImagingKindAsync(scope, kind, ct);

/// <summary>
/// One track for main rotation (SPEC F41, closes gitea-#210, gitea-#213) β€” a tiered preference query, not a
/// hard exclusion. Prefers, most-binding first: (1) an id not in <paramref name="orderedRecentIds"/>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,34 @@ public interface IPatterDurationEstimator
/// observed back in.
/// </summary>
void ObserveRendered(SegmentKind kind, string? personaName, string voice, TimeSpan measured);

/// <summary>
/// SPEC F117.2 (STORY-309, PLAN T250 review finding F1) β€” <see cref="Estimate(SegmentKind,string?,string)"/>'s
/// show-aware sibling: a genuinely NEW interface member, never that one widened in place (the SAME
/// binary-compat/orphaned-implementer reasoning that governs every other addition to this published
/// <c>GenWave.Abstractions</c> contract β€” see <see cref="IMediaCatalog"/>'s own F117 addition for
/// the fuller rationale). <paramref name="showName"/> lets an implementation
/// key its Exact tier on the RENDERED-TEXT identity for a <see cref="SegmentKind.StationId"/> segment
/// whose copy now varies by on-air show (F117.2's templated show line β€” "You're listening to
/// {show} on {station}." β€” is a DIFFERENT clip than the plain "You're listening to {station}."), so
/// a show-branded observation can never be reported back as the Exact duration for an unrelated
/// plain (or differently-shown) airing.
/// <para>
/// Default-implemented so this stays strictly additive: any implementer that has not opted in
/// (only <c>RollingPatterDurationEstimator</c> does, today) degrades to the 3-arg overload with
/// <paramref name="showName"/> simply dropped β€” exactly its own pre-F117 behavior, unchanged.
/// </para>
/// </summary>
PatterDurationEstimate Estimate(SegmentKind kind, string? personaName, string voice, string? showName) =>
Estimate(kind, personaName, voice);

/// <summary>
/// SPEC F117.2 (STORY-309, PLAN T250 review finding F1) β€” <see cref="ObserveRendered(SegmentKind,string?,string,TimeSpan)"/>'s
/// show-aware sibling, the write-side counterpart to <see cref="Estimate(SegmentKind,string?,string,string?)"/>
/// above; see that member's own remarks for why this is additive rather than an in-place widening,
/// and for what <paramref name="showName"/> means. Default-implemented the same way: an implementer
/// that has not opted in degrades to the 4-arg overload with <paramref name="showName"/> dropped.
/// </summary>
void ObserveRendered(SegmentKind kind, string? personaName, string voice, TimeSpan measured, string? showName) =>
ObserveRendered(kind, personaName, voice, measured);
}
35 changes: 34 additions & 1 deletion src/GenWave.Abstractions/Domain/SegmentRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@ namespace GenWave.Core.Domain;
/// The same crossing track's artist, alongside <see cref="CrossingTrackTitle"/> β€” <see langword="null"/>
/// whenever that is.
/// </param>
/// <param name="ShowName">
/// SPEC F116.2 (STORY-307, PLAN T248) β€” for a <see cref="SegmentKind.SignOff"/>/<see cref="SegmentKind.SignOn"/>:
/// this piece's OWN show, carried verbatim from the deferral's own captured
/// <c>HandoffContext.ShowName</c> (never re-derived here). <see langword="null"/> for a showless
/// boundary β€” additive and optional so every existing caller is diff-free.
///
/// <para>
/// SPEC F117.2 (STORY-309, PLAN T250) β€” ALSO rides on a <see cref="SegmentKind.StationId"/> request
/// when the drain is firing during a show and the authored imaging pool came up empty: the
/// Orchestrator's own drain arm stamps the on-air show's name here, and
/// <c>GenWave.Tts.PatterTemplateRenderer.Expand</c>'s <see cref="SegmentKind.StationId"/> arm
/// renders "You're listening to {ShowName} on {StationName}." instead of the plain
/// "You're listening to {StationName}." <see langword="null"/> (the F110.2-original, byte-identical
/// phrasing) outside a show, or whenever an authored pool row already served the drain.
/// </para>
/// </param>
/// <param name="ShowFlavor">
/// SPEC F116.2/F115.3 β€” <see cref="ShowName"/>'s own flavor text, carried the same way; populated only
/// on a <see cref="SegmentKind.SignOn"/> request (F116.2 names flavor for the sign-on prompt alone).
/// Reaches the LLM prompt ONLY β€” never a public payload or a log line (F115.3, the persona-soul
/// precedent) β€” <c>GenWave.Core.Domain.ShowSummary.Flavor</c> and <c>GenWave.Orchestration.HandoffContext.ShowFlavor</c>
/// both carry the same warning this field does: this record's own compiler-generated
/// <c>ToString()</c> renders it verbatim, so no <c>{Request}</c>-style structured-log placeholder may
/// ever bind a <see cref="SegmentRequest"/> on any public-adjacent logging path; log
/// <see cref="PersonaName"/>/<see cref="ShowName"/> by name instead.
/// </param>
/// <param name="CounterpartShowName">
/// SPEC F114.3/F116.2 β€” the OTHER piece's show, carried the same way; populated only on a
/// <see cref="SegmentKind.SignOff"/> request ("sign-off may name the ending show and the next").
/// </param>
public sealed record SegmentRequest(
SegmentKind Kind,
string Voice,
Expand All @@ -63,4 +93,7 @@ public sealed record SegmentRequest(
string? CounterpartName = null,
string? ContextFacts = null,
string? CrossingTrackTitle = null,
string? CrossingTrackArtist = null);
string? CrossingTrackArtist = null,
string? ShowName = null,
string? ShowFlavor = null,
string? CounterpartShowName = null);
41 changes: 41 additions & 0 deletions src/GenWave.Core/Abstractions/IShowFlavorLineSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace GenWave.Core.Abstractions;

using GenWave.Core.Domain;

/// <summary>
/// SPEC F116.3 (STORY-308, PLAN T249) β€” the show-flavor patter line's own internal seam: at most one
/// due show line for the on-air copywriter's prompt, mirroring <see cref="IContextPatterFactSource"/>'s
/// own shape and reason for existing one seam over. Deliberately NOT part of the published MIT
/// <c>GenWave.Abstractions</c> surface (F105.6): nothing outside this codebase's own patter lane
/// (<c>GenWave.Tts.LlmCopyWriter</c>, PLAN T249) ever needs to consume it, so it lives one layer in,
/// alongside <see cref="IContextPatterFactSource"/>.
///
/// <para>
/// <c>GenWave.Orchestration.ShowFlavorLineGate</c> is the one production implementation β€” show
/// identity is Orchestration's own domain (<c>OnAirPersonaAccessor</c>, <c>CachingScheduleResolver</c>,
/// <c>HandoffContext</c> all already live there) β€” and this interface is what lets
/// <c>GenWave.Tts</c> depend on the CONTRACT without ever taking a project reference to
/// <c>GenWave.Orchestration</c> itself, the exact same L1 reason <see cref="IContextPatterFactSource"/>
/// lives here rather than beside <c>GenWave.Context.ContextPipeline</c>. <see cref="NoOpShowFlavorLineSource"/>
/// is the safe default until a host wires the real binding.
/// </para>
/// </summary>
public interface IShowFlavorLineSource
{
/// <summary>
/// Returns the show-flavor line due right now, or <see langword="null"/> when none is due β€” no
/// show on the air, the show carries no flavor text, <c>Station:Shows:PatterCadenceMinutes</c> is
/// 0 (off) or has not yet elapsed for THIS show, or the caller simply never asks (SPEC F116.3's own
/// arbitration: "context wins... the show gate stays open for the next eligible break" β€” never a
/// reason to fail a render).
///
/// <b>This is a CONSUMING read, not a peek.</b> A non-null return is marked delivered for its
/// show's cadence window and will not be returned again until that window elapses β€” calling this
/// twice in immediate succession for the same due show yields it once and <see langword="null"/>
/// the second time. A caller that must not spend the slot (SPEC F116.3's own "context wins" β€”
/// see <c>GenWave.Tts.LlmCopyWriter.TakeDueShowFlavorLineForOnAirRender</c>'s remarks) must not call
/// this method at all, rather than calling it and discarding the result β€” discarding still burns
/// the slot.
/// </summary>
ShowFlavorFact? TryTakeDueShowLine();
}
26 changes: 26 additions & 0 deletions src/GenWave.Core/Abstractions/IShowPatterCadenceProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace GenWave.Core.Abstractions;

/// <summary>
/// SPEC F116.3 (STORY-308, PLAN T249) β€” the thin accessor seam between
/// <c>GenWave.Orchestration.ShowFlavorLineGate</c> (which cannot see the Host's
/// <c>IOptionsMonitor&lt;StationOptions&gt;</c> directly) and the Host's live
/// <c>Station:Shows:PatterCadenceMinutes</c> value. Mirrors <see cref="ICadenceProvider"/> one seam
/// over: <c>Station:Shows:PatterCadenceMinutes</c> is advertised <c>Live</c> in the settings allowlist,
/// so an operator edit reaches the very next eligible break with no process restart.
///
/// <para>
/// Implementations MUST re-evaluate <see cref="PatterCadenceMinutes"/> fresh on every read β€” never
/// cache it in a field β€” the same discipline <see cref="ICadenceProvider.Current"/> follows. No NoOp
/// default is registered anywhere: this seam has exactly one consumer (<c>ShowFlavorLineGate</c>),
/// which is itself only ever bound by the Host in the SAME registration that supplies the real
/// <c>OptionsMonitorShowPatterCadenceProvider</c> binding (mirrors <see cref="ICadenceProvider"/>'s own
/// mandatory-dependency posture on <c>Orchestrator</c> β€” no fallback exists for that seam either).
/// </para>
/// </summary>
public interface IShowPatterCadenceProvider
{
/// <summary>The live cadence, in minutes, at which the show-flavor patter line may air per show
/// (SPEC F116.3); 0 disables it (the default β€” an opt-in feature). Evaluated fresh on every
/// call.</summary>
int PatterCadenceMinutes { get; }
}
24 changes: 24 additions & 0 deletions src/GenWave.Core/Abstractions/NoOpShowFlavorLineSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace GenWave.Core.Abstractions;

using GenWave.Core.Domain;

/// <summary>
/// The default <see cref="IShowFlavorLineSource"/> binding (SPEC F116.3, STORY-308): always answers
/// "no line due" β€” mirrors <see cref="NoOpContextPatterFactSource"/>'s own "safe default until a real
/// seam is wired" idiom one seam over. <c>GenWave.Tts.TtsServiceCollectionExtensions</c> registers this
/// with <c>TryAddSingleton</c> so the Host's real <c>GenWave.Orchestration.ShowFlavorLineGate</c>
/// binding, once wired, overrides it without <c>GenWave.Tts</c> ever needing a project reference to
/// <c>GenWave.Orchestration</c> (an L1 project one layer further out) at all. Until that wiring lands β€”
/// or for any composition that never registers a show-flavor gate at all (a unit test, a station with
/// no schedule store configured) β€” every render behaves exactly as it did before F116.3: the patter
/// lane's own byte-identical-with-no-line golden (PLAN T249) is what pins that this default is inert,
/// not merely absent.
/// </summary>
public sealed class NoOpShowFlavorLineSource : IShowFlavorLineSource
{
/// <summary>Shared instance for non-DI construction (Core types, tests).</summary>
public static readonly NoOpShowFlavorLineSource Instance = new();

/// <inheritdoc/>
public ShowFlavorFact? TryTakeDueShowLine() => null;
}
18 changes: 18 additions & 0 deletions src/GenWave.Core/Domain/ShowFlavorFact.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace GenWave.Core.Domain;

/// <summary>
/// At most one due show-flavor line for the current break's patter prompt (SPEC F116.3, STORY-308,
/// PLAN T249) β€” <see cref="Abstractions.IShowFlavorLineSource.TryTakeDueShowLine"/>'s return shape, and
/// <c>GenWave.Orchestration.ShowFlavorLineGate.TryTakeDueShowLine</c>'s own return shape (that class
/// implements the interface above; Orchestration references Core, never the reverse β€” see that
/// interface's own remarks for why this record lives HERE, in Core, mirroring
/// <see cref="ContextPatterFact"/>'s own placement one seam over). Deliberately minimal and internal to
/// the repo, not part of the published MIT <c>GenWave.Abstractions</c> surface (F105.6): nothing
/// outside this codebase's own patter lane (<c>GenWave.Tts.LlmCopyWriter</c>, PLAN T249) consumes it.
/// </summary>
/// <param name="ShowName">The on-air show's display name, verbatim off
/// <c>Abstractions.Playout.OnAirSnapshot.Show</c> β€” never re-derived.</param>
/// <param name="Flavor">The show's flavor text, verbatim off that same snapshot. The gate never hands
/// out a fact with blank flavor (nothing to say), so this is always non-blank when this record
/// exists.</param>
public sealed record ShowFlavorFact(string ShowName, string Flavor);
Loading
Loading