diff --git a/CHANGELOG.md b/CHANGELOG.md index e95f1cda..0e85f35f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,3 +75,4 @@ is at the bottom; the current roadmap lives in the [README](README.md#roadmap). - **v5.4.0 — The DJ stops lying, and the house gets a voice (SPEC F138–F147 · gh-#434, gh-#438, gh-#384, gh-#528, gh-#610, gh-#620)** — two epics and the sweep that cleared the deck for them. **The truth lane (F138–F142, PRs #605/#609)**: every LLM-authored line now passes a **truth gate** at the `LlmCopyWriter` seam — invented facts, wrong weekdays, wrong dayparts, fabricated verifiables are caught, re-asked exactly once, and template-floored if the model lies twice (the aired "Saturday morning… tonight" of gh-#438, gone); **every LLM call carries a typed cause** (`Success | Timeout | OverLength | TruthGateReject | ConnectionFailure | CanceledByWindow | EmptyCompletion | MalformedResponse`) with rolling per-model counters, so a red LLM tile finally *names* its cause and model instead of just being red (gh-#365); the **honest time signal** (F141: a 420 s budget and a "just past" variant, never an invented hour — gh-#526), the **boundary cadence covenant** (F142, gh-#300), and the **gap-aware crosstalk stock** (F140: runway projection + exponential backoff, gh-#546). **The model floor, measured (gh-#438, T336, #627)**: a week of those counters on the bench — five models, ~1.1k gated calls a day — put *every* model above the floor (worst share 0.3%); the answer was prompt shape, not size or family, and HARDWARE.md now says so. **The House Voice (F143–F147, PR #619 + `genwave-homeassistant` v0.1.0)**: owner announcements are a durable unit of content with a total, visible state machine (`station.announcement`, db/40) — `POST /api/announcements` (280 chars, 6/min, depth 12, case-folded collapse), a dedicated hashed reveal-once **announce token**, the DJ works a casual message into the next break in character or reads it **verbatim** when asked (and *always* verbatim when the flavored copy fails the gate — the fallback law), `aired` stamps only on the real `TrackAired`, a lifecycle guardian re-arms the claimed-but-never-aired and expires the stale, the **Announcements** admin page, and a hard **403 while the station is public** (SpectatorMode) so a public stream never carries the house's events. On the Home Assistant side: a HACS custom integration with the `genwave.announce` service, a `notify` entity, a **now-playing sensor**, and a starter blueprint gallery (dinner bell, laundry done, morning ramp) — gated on 2026-08-28 by a real automation on the maintainer's own HA ringing the DJ within one break cycle. **The silent-loss class (gh-#610 → gh-#611/#612, PRs #615/#616)**: a catalog row the scan can never re-verify (a library discovered under a previous root) is **quarantined** after the miss grace instead of feeding the engine files that don't exist; pushes check `File.Exists` first, and a pushed-never-aired chain now WARNs — the canary station's 9,112 ghost rows cleared themselves two scans after boot, no SQL. **Thinking models work (gh-#620, #630)**: `Llm:ReasoningEffort` (live, default `none`, `omit` for a backend that rejects the field) rides every completions request, and a reasoning-only reply is named as such in the WARN instead of "empty after cleanup" — gemma4/qwen3/deepseek-r1/magistral no longer return empty copy. **The 08-20 bug bash (#596–#604)**: speakability flatten at the F68 chokepoint (gh-#541), optimistic-concurrency guard on whole-array settings writes (gh-#486, db/38), the speaking-DJ avatar enlarges on the spectator page (gh-#582), a real ESLint gate for the admin UI (gh-#553), the ffmpeg-install composite action (gh-#588), and per-run-unique compose projects for both test fixtures (gh-#569/#602). Schema: **db/38, db/39, db/40** — `migrate.sh` on the way up. Abstractions **5.4.0**: additive — `SegmentKind.Announcement` and `TimeAnnouncementFreshness` on `SegmentRequest`. - **v5.4.1 — The gate learns the station's name, the sensor learns the appliance (SPEC F138.8, F144.3 amended, F145.6 · gh-#632)** — patch: the first real House Voice airing's findings, closed the same day. **The flavored lane can actually air (gh-#632)**: the truth gate treated the station's own name as an invented number — `GWAV 108.8` on the announcement lane read as an unsupported "108.8" — and refused the owner's sentence over a changed exclamation mark (`hot.` vs `HOT!`). Now the station's name is a **supported fact on every lane** (its digit runs, weekdays *and* condition words — a station named "Sunny 101.5" no longer trips the weather-word check; names are whitespace-collapsed before the span search; the crosstalk parser's shape checks get the same exemption), and core containment compares **word sequences** — hygiene-normalised, case-folded, Unicode letters and numerals with apostrophe folding, punctuation is not a word, the owner's words contiguous and in order. Case and punctuation are free; words are not: a paraphrase still falls to the verbatim floor, and attempt-one fabrication still dies. Proven through the production binary with `Station:Name` read from the host's own options — a station renamed "DRIFTED FM 99.9" is refused for exactly the digit run. **The now-playing sensor works on an appliance (F145.6)**: `GET /api/announcements/now-playing` moves onto its own controller with no admin-surface gate, so Home Assistant's sensor answers while `Admin:Enabled` is off (the public appliance's normal posture), public or private; submit, history and the token endpoints stay admin-surface (404), no token still fails closed (401), and the read stays off the public listener port. Mint the announce token during the temporary-admin window. **Fitness laws**: L9 (the announce-scheme fence) widens to exactly the two named carriers and reds on a third; **L10 — namespace cycle-freedom — is now a real law** reporting through `LawId.L10` over the production assemblies (a planted cycle reds), with its CONTRIBUTING row. Test infrastructure: the ephemeral-Postgres harness (`Host.Tests/Support/EphemeralStationDatabase`) and the announcement wire helpers are shared, not copied. No schema, no contract change; Abstractions 5.4.1 is byte-equivalent to 5.4.0. Filed on the way: gh-#635 (a 405 leaks admin-surface path existence with the admin plane off), gh-#636 (test hygiene). Companion: `genwave-homeassistant` README (PR #2) states the appliance posture. - **v5.5.0 — The library tends itself (SPEC F150–F155 · gh-#529)** — the Library Gardener: a `BackgroundService` that houses five rot passes on a timer and a rotation signal fed by listener taste. **Five passes, one queue**: `dead_file`, `near_duplicate`, `stale_metadata`, `shelf_dust` (unaired past `Gardener:ShelfDustDays`), and `unreachable` (admitted by no weekly-grid envelope) each reconcile set-based against `library.rot_finding` (db/41) — no file reads of their own, dead-file evidence comes from the scanner's existing stat pass — surfaced on a new **Gardener page** (per-kind, near-duplicate rows grouped by pair, dismiss/eligibility/never-play/re-enrich/purge verbs) and a **dashboard tile** with live open counts. **The rotation nudge (F150)**: listeners can thumb the current track from the spectator page (`Station:Thumbs:Enabled`, a *live* setting, off by default) alongside the existing operator taste thumbs — both write `library.media_thumb` under the same `ThumbSource`-tagged row, decayed by half-life (`Gardener:HalfLifeDays`) and saturation (`Gardener:Saturation`) into a `[-1, 1]` nudge on `library.media_rotation` that adds into the persona ranker's rung-0 score (`Gardener:NudgeGain`). **Deep Cuts (F152)**: a show's own rotation predicate (`MaxPlays`/`NotAiredWithinDays`, `PUT /api/shows/{id}`) filters candidates by construction, ahead of the ordinary F81.6 envelope ladder, with its own **R0→R3 relax ladder** so a thin catalog under a tight predicate never starves a boundary instead of straddling it. **File actions are opt-in (F154)**: retag/rename/move — no delete verb — behind `Gardener:FileActions:Enabled` (default off) *and* the new `compose.fileactions.yaml` overlay (widens only the api's own `/media` mount to `:rw`); every write is jailed to `MediaRoot`, never overwrites, and a retag swaps in behind a same-directory `.gwbak` backup of the original — a failed revert leaves that backup as the only surviving original, and further retags on the file refuse until an operator resolves it. Schema: **db/41** (`library.media_rotation`, `library.media_thumb`, `library.rot_finding`, `library.file_action`, the `rot_kind`/`rot_state` enums) — `migrate.sh` on the way up. Abstractions **5.5.0** is a **minor** bump: `ScheduleSegment.EffectiveEnvelope` is additive, extracted so the Gardener's own `unreachable` pass and `ScheduleResolver` share byte-identically the same per-field envelope-fallback formula instead of two independent copies drifting apart. Filed on the way: gh-#649 (dev-box test fixtures can exhaust Docker's default address pools under a full-parallel `dotnet test` run — CI unaffected, runs per-project), gh-#650 (a directory symlink inside `MediaRoot` aliasing another directory makes the scan double-count one file as a ghost `discovered` row; the file-action planner already refuses moves into a symlinked target, the scan-side dedupe is the open half). +- **v5.5.1 — The queue at scale (gh-#654/#655/#657 · SPEC F153.9/F153.10 riders)** — patch: the Gardener's first contact with a real library (the demo's 1,532 dead-file corpses) proved the findings page's one flat 1000-row fetch a starvation defect — ordered by kind with `dead_file` declared first, it consumed the whole page and rendered "Showing first 0 of 233" on every other section (gh-#654). The page rebuilds on the catalog idiom: server-rendered `?tab=&page=&limit=`, five badged kind tabs, "Page N of M" pagination with a 25/50/100/250 size picker (default 25, URL-only), verbs refreshing the server truth via `router.refresh()`. **Near-duplicates page by whole group** — `limit`/`offset` count distinct clusters and a page can never split one — with member semantics the review loop proved from both directions on live Postgres: a **dismissed** member still renders with its cluster (it is still a duplicate; hiding it let *Keep this one* leave the hidden sibling eligible), while a **resolved** member never does (the resolve keeps its stale `group_key`, and rendering it let *Keep this one* bulk-ineligible a track that is no longer a duplicate). Kind-scoped `GET /api/gardener/findings` now carries an exact `total` (groups for near-duplicates, rows otherwise); un-scoped callers keep the old response byte-for-byte. The purge trigger reads **"Purge dead tracks…"** — a verb, not a status (gh-#655). Page numbers clamp inside Int32 so paging params stay hints, never 400s, and the pager becomes one shared component (the catalog page converts to it, markup byte-identical). Left open on purpose: gh-#656 — the purge tripwire still refuses a legitimate whole-library-replacement cleanup; its typed-confirmation override gets its own design. diff --git a/compose.pinned.yaml b/compose.pinned.yaml index 6913ba8b..94ce73c2 100644 --- a/compose.pinned.yaml +++ b/compose.pinned.yaml @@ -24,13 +24,13 @@ services: admin_ui: - image: ghcr.io/genwave-org/genwave-admin-ui:home-v5.5.0 # bump per release + image: ghcr.io/genwave-org/genwave-admin-ui:home-v5.5.1 # bump per release pull_policy: always # `!reset` drops the base `build:` context entirely — see the file header for why. build: !reset null engine: - image: ghcr.io/genwave-org/genwave-engine:home-v5.5.0 # bump per release + image: ghcr.io/genwave-org/genwave-engine:home-v5.5.1 # 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 @@ -44,15 +44,15 @@ services: # 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-v5.5.0 # bump per release + image: ghcr.io/genwave-org/genwave-piper:home-v5.5.1 # bump per release pull_policy: always # Same !reset rationale as admin_ui above (gh-#93). build: !reset null icecast: - image: ghcr.io/genwave-org/genwave-icecast:home-v5.5.0 # bump per release + image: ghcr.io/genwave-org/genwave-icecast:home-v5.5.1 # bump per release pull_policy: always api: - image: ghcr.io/genwave-org/genwave:home-v5.5.0 # bump per release + image: ghcr.io/genwave-org/genwave:home-v5.5.1 # bump per release pull_policy: always