F107–F109: the context seam — IContextProvider, weather, history (PR 2 of 4) - #418
Merged
Conversation
…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.
This was referenced Aug 9, 2026
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.
📻 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), theGenWave.Contextpipeline (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:ICadenceFlooredContextProvider).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.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.
After merging + deploying, verify on the box:
Station:Location:Latitude/Longitude/SpokenName+Context:Weather:Enabled=true(Settings page, live).select occurred_at, segment_kind, summary from station.booth_log where segment_kind='ContextSegment' order by occurred_at desc limit 5;).Context:History:Enabled=true⇒ history airs from cache (check{CacheRoot}/context/history/gains today+tomorrow JSON).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.