diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf7aa14..36d4972f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,3 +60,4 @@ is at the bottom; the current roadmap lives in the [README](README.md#roadmap). - **v3.2.0 — Wardrobe workshop M2: the editor (SPEC F104, PRs #395/#396)** — the theme font law widens to **vendored ∪ installed** (T205): `ThemeFontProvenanceValidator`'s per-theme byte ceiling now sums both sets, and an import 400 names the missing face plus, when the catalog index knows a pack that provides it, the pack's own slug (fail-soft — catalog CI stays curated-only, so shipped themes remain structurally incapable of referencing a pack). The **theme editor** (`/editor`, T206) mixes a base theme's palette with a role face (display/sans) over one server-derived assignable set (`GET /api/fonts/assignable`, vendored ∪ installed in one union), previewing through the T186 transient-compose mechanism — nothing persists until **save-as-own** (T207) writes it to `station.theme` behind a shared `ThemeWriteGate` (one two-phase pipeline now serves both write routes, gate-identical by construction) with a **fail-closed 409** refusing to overwrite an already-imported theme's provenance. Font pack **uninstall** (T208) removes a pack unless a saved/imported theme still references one of its faces — the guard lives *inside* the `DELETE` statement itself (`WHERE NOT EXISTS`), not an advisory pre-check a concurrent write could race past; refusal names every referencing theme. T209 re-audits public disclosure over the widened surface (self-derived AdminSurface+Settings sweep, the one sanctioned spectator change pinned). T210's exit-demo — mix, save, activate on the pinned v3.2.0 demo box — closed F104. - **v3.3.0 — A station, not a playlist (SPEC F107–F113, project #3's flavor floor)** — the content kinds radio is made of, on one seam. **The context seam (F107)**: `IContextProvider` joins the MIT contract surface (the demonstrated F105.6 need is GenWave Business) with implementations in the new **`GenWave.Context`** project — born outside Host per the F105.4 graduation rule, covered by every fitness law from day one. One pipeline (fetch-once-per-cadence-slot, freshness re-checked on every read, **skip-never-silence** — a dead provider is inaudible), two delivery lanes: rendered **context segments** at track boundaries via the F74 queue (supersede now per `(kind, discriminator)` so due weather never erases due history) and an **at-most-one, cadence-gated context fact** in LeadIn/BackAnnounce prompts — byte-identical prompts otherwise, golden-pinned. Provider text is **fenced as data-not-instructions** end to end: the sanitizer makes fence delimiters structurally unrepresentable (adjacent identical angle brackets cannot survive it), lengths capped, newlines dead — adversarially proven against crafted Wikimedia payloads. **Weather (F108)**: Open-Meteo, keyless, SSRF-closed (validated invariant coordinates into a fixed host — nine injection shapes fail closed with zero outbound requests); the 2026-07-20 spoken-vs-precise ruling enforced — coordinates reach no prompt, payload, or log line, `Station:Location:SpokenName` is the only place name a DJ may speak; blank coords = provider off even when enabled, edge-logged once. **This day in history (F109)**: Wikimedia On-This-Day (curated `selected` feed), **paraphrase-only** under the news posture — a 3B model never free-recalls dates on air; day-file cache + next-day pre-fetch so the segment survives restarts and outages; F76-style etiquette UA shared with MusicBrainz. **Idents & time (F110)**: opt-in clock-anchored top-of-hour idents drain **pool-first** — the gh-#149 imaging collection gets its first playout consumer (random ready authored `station_id` row airs as audio; templated TTS fallback), and `SegmentKind.TimeDate` gains its first-ever producer — *"It's two o'clock."*, station-voiced, zero LLM, per-hour text = forever-cache hit; F95.4 tightened along the way (an explicit-marked ident never airs on an `everyone` station — fail-closed to the template, never dead air). **The straddle handoff (F111, gh-#320's middle rung — and gh-#300 closed with it)**: when nothing in rotation fits the boundary and the picked track **provably crosses** (measured duration only — never inferred), the outgoing DJ signs off *into* it and the incoming DJ picks up on the far side, back-announcing the captured track; ceremony-now demotes to a genuine last resort. **The pick pipeline left the Orchestrator first (F112, gh-#401)**: `MusicSelectionPolicy` owns request rung → persona rung → trust-but-verify → degradation ladder → boundary resample, returning a `Fit | Straddle | CeremonyOnly` outcome with **one floor authority** and rung-tagged fit logging (F111.5). **Airings became countable (F113)**: `booth_log.segment_kind` stamped at *air* time (a budget-dropped render never counts; db/33, which also lands the dormant `station.show` schema for the dayparting slice) — the epic's demo-hour gate is one indexed query, shipped as `tools/demo_hour_gate.sql`. Built via /build-loop's adversarial gate: **every substantive task failed round one** and every catch was mutation-proven before passing — the sim that showed 70–92% of hourly idents silently erased (now 0.0%), sign-offs airing 4–6 minutes early on short-track pools, and a prompt fence a Wikimedia editor could close from the outside. - **v3.3.1 — The whole sentence airs (gh-#424)** — patch: cue-out now comes only from silence that **actually extends to EOF**. `FfmpegCueAnalyzer` took the *last* silence region as cue-out unconditionally, so an interior pause that happened to be last — a 0.6 s injected TTS sentence gap (they clear the 0.5 s silencedetect floor), a quiet mid-track break — became `liq_cue_out`, and **the final sentence of every multi-sentence patter clip was cut on air** while booth logs, the cached wav, and the preview all looked complete (`DurationMs` derives from the same cue-out, so even the progress bar agreed). Trailing now means open-ended at EOF or closed within 0.25 s of the container duration; interior-only files report the full audible extent so durations stay measured for straddle boundary-fit; misclassification degrades safe-only (untrimmed tail → `blank.eat` backstop). Analyzer-level on purpose: music with a quiet interior break stops mis-trimming too. Playout self-heals on deploy (blurb cues re-measure per process); persisted cue rows — music enrichment, authored imaging — keep old values until re-enriched/re-authored. Found in Dean's T229 listening pass (both v2.9.0 and v3.3.0 sightings were this one path); regression spec red-checked against the pre-fix parser. +- **v3.3.2 — Nothing grim, nothing clipped (gh-#433, gh-#424 rollout)** — patch, two halves. **The history airability gate (gh-#433)**: `HistoryFactHygiene` sits between the On-This-Day day cache and either delivery lane at **vend** time — somber facts (violent-death / disaster / atrocity vocabulary, word-boundary matched, deliberately fail-closed) never air, and half-stripped `[[wikilink]]` bracket residue is cleaned from what does. Driven by a live sighting: a fatal plane crash aired as chill-morning patter color, stray bracket included. An all-somber day — Wikimedia's curated `selected` feed skews grim; the real captured reply was five-for-five — is an ordinary F107.6 skip, one Information line; pool-widening to the fuller `events` feed is the recorded follow-up if coverage thins too far. **The stale-cue retirement (db/34)**: every cue point persisted before v3.3.1's analyzer fix could carry a mid-file `cue_out` (music with a quiet break clipped at the pause — heard live on the demo). A marker-guarded one-time migration (`library.one_time_fix` ledger, claim-CTE, twice-run-proven against a real postgres:16.4) nulls the stale cue columns at deploy — instantly curative, NULL cue = full-file playback — and the F13 backfill predicate re-measures the whole library with the fixed analyzer at the worker's own pace. Zero operator action, per the standing zero-manual-release-steps rule. diff --git a/compose.demo.yaml b/compose.demo.yaml index e599221f..0a98b2dc 100644 --- a/compose.demo.yaml +++ b/compose.demo.yaml @@ -47,7 +47,7 @@ services: restart: unless-stopped admin_ui: - image: ghcr.io/genwave-org/genwave-admin-ui:home-v3.3.1 # bump per release + image: ghcr.io/genwave-org/genwave-admin-ui:home-v3.3.2 # bump per release pull_policy: always # `!reset` drops the base `build:` context entirely: a demo box must pull this image or # fail loudly — never silently fall back to building (and then forever running) local @@ -61,7 +61,7 @@ services: - "127.0.0.1:3000:3000" engine: - image: ghcr.io/genwave-org/genwave-engine:home-v3.3.1 # bump per release + image: ghcr.io/genwave-org/genwave-engine:home-v3.3.2 # bump per release pull_policy: always # Same !reset rationale as admin_ui above (gh-#93). Note genwave.liq is a bind mount # from the checkout (see compose.yaml), NOT baked into this image — the image carries @@ -74,7 +74,7 @@ services: # silently source-build and freeze. NOTE the first genwave-piper publish happens at the FIRST # TAG CUT AFTER gh-#240/#241 — until that release's pin bump this tag does not exist and the # pull fails loudly by design. Bump with the other four pins per release. - image: ghcr.io/genwave-org/genwave-piper:home-v3.3.1 # bump per release + image: ghcr.io/genwave-org/genwave-piper:home-v3.3.2 # bump per release pull_policy: always # Same !reset rationale as admin_ui above (gh-#93). build: !reset null @@ -123,7 +123,7 @@ services: restart: "no" icecast: - image: ghcr.io/genwave-org/genwave-icecast:home-v3.3.1 # bump per release + image: ghcr.io/genwave-org/genwave-icecast:home-v3.3.2 # bump per release pull_policy: always # Base compose publishes 8000:8000 on 0.0.0.0 — right for LAN listening in # Operator/Standard, wrong on a public box: Icecast serves status.xsl (listener @@ -134,7 +134,7 @@ services: ports: !override [] api: - image: ghcr.io/genwave-org/genwave:home-v3.3.1 # bump per release + image: ghcr.io/genwave-org/genwave:home-v3.3.2 # bump per release pull_policy: always # compose.yaml's base `ports:` publishes BOTH 8080 and 8081 on 0.0.0.0 — fine for # Operator/Standard (LAN reachability is the point there), wrong for a public box.