Skip to content

F107–F109: the context seam — IContextProvider, weather, history (PR 2 of 4) - #418

Merged
genwave-radio merged 9 commits into
mainfrom
feat/f107-context-seam
Aug 9, 2026
Merged

F107–F109: the context seam — IContextProvider, weather, history (PR 2 of 4)#418
genwave-radio merged 9 commits into
mainfrom
feat/f107-context-seam

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

📻 A station, not a playlist — PR 2 (T221–T228 + T226; stacked on #416)

The seam goes live. IContextProvider (Abstractions/MIT — the F105.6 need is GenWave Business, ruled at /design), the GenWave.Context pipeline (8th src project, born outside Host, L1/L3/L5-covered), per-(kind, discriminator) deferral supersede, the boundary drain arm + one-fact patter lane (no-fact prompts byte-identical, golden-pinned), and both v1 providers:

  • Weather (Open-Meteo): SSRF-closed fixed host, coords validated/invariant/range-checked (nine injection shapes fail closed), spoken-vs-precise split leak-swept, fail-closed edge-logged once, 30-min floor enforced at write time AND structurally (ICadenceFlooredContextProvider).
  • History (Wikimedia On-This-Day): day-file cache + pre-fetch + sweep, station-local dates, F76-style UA (shared EtiquetteUserAgent), the prompt fence — provider text is sanitized at one chokepoint (angle-bracket runs collapse; <<</>>> unrepresentable) and fenced as data-not-instructions in both lanes. Review round 1 proved the naive fence escapable with a crafted Wikimedia edit; the escape payload is now a Theory.
  • Wire: ContextTickerService (sequential, one wall-clock actor), Host shims, 13 Live settings, SEAMS.md at 72+ seams.

Review gate

Every task builder→reviewer(opus) with re-review; T222 (7 findings incl. an F105.6 placement reversal), T224 (4), T226 (vacuous-spec catch + floor honesty), T227 (3 rounds: unit-string injection, patter-lane fail-open), T228 (fence escape) all failed round one — every catch mutation-proven before PASS.

⚠️ T229 — live acceptance rides YOUR deploy (Dean's ruling)

After merging + deploying, verify on the box:

  1. Set Station:Location:Latitude/Longitude/SpokenName + Context:Weather:Enabled=true (Settings page, live).
  2. Within a cadence slot: a weather segment airs at a track boundary (listen, or select occurred_at, segment_kind, summary from station.booth_log where segment_kind='ContextSegment' order by occurred_at desc limit 5;).
  3. The next break's patter carries the conditions fact once (booth log / LLM inspector), and the break after is fact-free (single-provider setup — with two providers enabled the facts may span two breaks; known + documented).
  4. Context:History:Enabled=true ⇒ history airs from cache (check {CacheRoot}/context/history/ gains today+tomorrow JSON).
  5. Disabled providers ⇒ zero Open-Meteo/Wikimedia lines in the api log.

Evidence

Full sln 0W/0E; 3,168 tests green across 7 projects; jest 872 green; seam-index gate green; live degraded boot verified (ticker ticks quietly, zero outbound, clean stop).

🤖 /build-loop (builder sonnet / reviewer opus), specs-first.

…ract (T221, STORY-296)

The MIT seam gh-#378 asked for: a Key (queue discriminator + settings
segment) and one fetch returning ContextContent? (segment facts, optional
patter fact, FreshUntil). Null = nothing to say, never an error; providers
return facts — never prose-for-air, never audio. Deliberately minimal;
extension rides default interface members / init-only additions.
Two Architecture.Tests facts pin the contract shape and its immutability
via the L4 detector. Carry-forwards to T222/T226: exception posture
sentence, Key uniqueness + charset rule with duplicate fail-fast,
settings-prefix wording, empty-SegmentFacts semantics.
Eighth src project, born outside Host (L5 reservation stays empty; L1
list + L2/L3 sweep + ArchUnitNET graph all gain it). ContextPipeline:
per-provider cadence slots (UtcTicks floor-division), fetch-once-per-slot
incl. failures, freshness re-checked on every read, skip-never-silence
with one Information line per slot naming key + cause kind only (F108.3
— never provider content or ex.Message), edge-triggered disabled logging
(gh-#338 precedent), lock-guarded ProviderState (ticker vs copywriter
threads), TryTakeDuePatterFact consuming-read, GeneratedRegex ^[a-z0-9-]+\z
key gate with ctor fail-fast. Settings seam IContextSettingsProvider →
Core/Abstractions per F105.6 (grandfathered placements are not precedent).
CONTRIBUTING L1 row gains Context.

Review carry-forwards: T226 ticker must be a sequential await-loop (no
overlapping TickAsync); CapturingLogger 4th copy wants a shared
test-support home; partial first slot noted for rate-limited upstreams.
…egmentKind.ContextSegment (T223, STORY-297)

SpeechDeferralQueue keys on (kind, discriminator) — null for every
pre-F107 producer (Story197 zero-diff, byte-identical), provider key for
context kinds, so due weather never silently supersedes due history.
Drain order extracted to one InDrainOrder helper (Due → Kind →
Discriminator ordinal), shared by peek + drain. SegmentKind gains
ContextSegment: PatterTemplateRenderer stays TOTAL (inert arm — the
preview endpoint accepted the new kind on day one and 500'd without it,
review-caught + probe-proven), a new theory walks the enum so the 8th
kind can't reopen the hole, piper-only EngineByKind seed map gains the
kind per its own pinned convention. Also restores src/GenWave.Context's
sln registration (missed in the T222 commit).
…07 segment lane (T224, STORY-297)

Drain arm: immutable-capture payload (HandoffContext precedent) with
drain-time freshness + blank-facts guards (both mutation-proven), persona
per Context:{Key}:PersonaId (null==0==on-air DJ, station voice the
further fallback — doc and code now agree), F108.3-clean skip lines
(key + fixed cause, never content), provider-key-attributed drop WARNs.
Copy path: IsLlmAuthored gains ContextSegment (blurbs cache routing),
facts block carries 'Use only these facts. Do not add facts.' (omitted
entirely when facts are blank), degraded-LLM contexts SKIP rather than
air template filler (TtsSegmentSource fresh-copy guard widened; renderer
stays total for Hard-mode routing). Preview endpoint behavior pinned:
ContextSegment routes to the LLM rung, 502 when unconfigured, through
the real DI-wired writer chain.

Riders to T225: BuildContextFactsLine null-branch coverage; segment-role
line coherence when facts absent. Carry-forwards: prompt fencing gates
T228 (Wikimedia text is attacker-influenceable); per-kind drain builder
extraction before T232.
IContextPatterFactSource (Core/Abstractions, NoOp default via the
IStationEventSink TryAdd precedent) feeds LlmCopyWriter.WriteAsync — the
single take site, LeadIn/BackAnnounce only (IsPatterFactKind is the one
source of truth), previews structurally cannot consume the break's fact.
No due fact ⇒ prompt byte-identical, pinned by a golden copied verbatim
from Story214's independently-authored pin. Fact text capped at
MaxSoulChars like every injected field (review-caught). ContextPatterFact
moved Context→Core/Domain (dependency direction). T224 riders closed:
null-facts branch covered, segment-role line stays coherent without
facts, EveryOtherKinds upgraded to a real byte pin, template LeadIn/
BackAnnounce still air past the widened guard. SEAMS.md regenerated
(72 seams / 6 projects).

Carry-forwards: T226 must bind ContextPipeline with plain AddSingleton
(TryAdd loses silently to the NoOp); T228 fencing scope includes the
patter lane; T229 single-provider qualifier (two providers due can span
two breaks' prompts).
…T227, STORY-299)

The F108 reference provider: fixed-host typed client (SSRF-closed —
coords parsed to double, range-checked, invariant-formatted; nine
injection shapes fail closed with zero outbound requests, spec-pinned
incl. the exact outbound URI), display units are pinned literals so the
remote payload structurally cannot forge fact text (current_units DTO
deleted — review-caught HIGH), spoken-vs-precise split enforced and
leak-swept across facts + logs, fail-closed coords edge-logged once via
the new internal ISelfGatingContextProvider gate (both lanes — the
patter lane stops vending the instant availability flips and the cache
clears on the unavailable edge, review round-3 interaction catch),
freshness 2h clears the default cadence so healthy stations log zero
skip lines, negative-zero temperatures collapse to 0. L3 seam list gains
the provider + the AddGenWaveContext extension, mutation-proven.
…-300)

F109: Wikimedia On-This-Day (selected feed — curated subset), day-file
cache with atomic writes + next-day pre-fetch + 7-day sweep (now also
reclaiming orphaned temp files), station-local dates via the clock seam,
F76-style version-stamped UA extracted to a shared EtiquetteUserAgent
helper (both MusicBrainz + Wikimedia pins mutation-covered).

THE FENCE (the ruling that gated F109): ContextFactSanitizer now owns
delimiter safety — runs of identical angle brackets collapse to one, so
'<<<'/'>>>' are structurally unrepresentable in sanitized output; both
prompt lanes fence provider text as data-not-instructions with the
instruction outside; sanitize runs inside the pipeline's fault guard at
the one chokepoint no provider can bypass (poisoned cache files
included). Round-1 review proved the naive fence escapable and two specs
certifying the hole — all inverted; the reviewer's exact escape payload
is now a Theory, and the full hostile-Wikimedia composition was verified
through the real provider+pipeline+prompt chain. No-fact prompt goldens
byte-identical throughout.
The one wall-clock actor: sequential PeriodicTimer loop (no overlapping
ticks — the pipeline's locking contract is ticker-vs-copywriter), never
throws out of ExecuteAsync, F74 one-boundary-late caveat carried onto
its first producer. Host shims: ConfigurationContextSettingsProvider
(named for what it wraps), StationLocation + ContextCacheRoot providers;
plain AddSingleton binds ContextPipeline over every NoOp (TryAdd would
lose silently — DI-probed: one singleton, patter lane and ticker share
the instance). 13 allowlisted Live settings with write-time validation —
weather's 30-min floor enforced at write (not silently clamped at read;
display == effective) AND structurally via the new
ICadenceFlooredContextProvider capability the pipeline consults.
Acceptance spec rebuilt after review proved it vacuous: real factory,
transport-only swap, positive control mutation-proven (Enabled gate
out ⇒ red). All 8 appsettings seeds pinned to C# homes (history's 240
included). Live degraded boot verified: ticks quietly, one edge line
per provider, zero outbound, clean stop.

Riders to T229: floor-consult spec + validator-asymmetry spec.
Base automatically changed from feat/f112-extraction to main August 9, 2026 14:22
@genwave-radio
genwave-radio merged commit 4660999 into main Aug 9, 2026
11 checks passed
@genwave-radio
genwave-radio deleted the feat/f107-context-seam branch August 9, 2026 14:27
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 9, 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