Skip to content

fix: taste-integrity cluster — safe-content exclusion, null-DaysOfWeek NRE, label precedence (gh-#99, #87, #89) - #100

Merged
genwave-radio merged 4 commits into
mainfrom
fix/taste-cluster-99-87-89
Jul 23, 2026
Merged

genwave-radio merged 4 commits into
mainfrom
fix/taste-cluster-99-87-89

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

Summary

Why

Filed as the taste-integrity cluster: #99 (P1, demo) lets operators teach personas artist rules for the station's own name and lets a never-play write depotentiate the never-silent fallback; #87 silently disables taste rules reachable via persona import; #89 was about to grow a fifth copy of the precedence chain under the #87 fix.

How to verify

Risk / rollback

Additive column (media_id, nullable, no FK, idempotent migration) and additive DTO fields — old rows read back as unstamped/rateable. Fail-closed only for safe-scope rows; empty safe scope is a no-op. Revert via git revert of the three commits.

Closes #99
Closes #87
Closes #89

🤖 Generated with Claude Code

genwave-radio and others added 4 commits July 23, 2026 09:52
…silently dying

A persona_taste context of {} deserializes TasteContext.DaysOfWeek to
null (STJ missing-property default) and TasteMatcher.MatchesDay NRE'd
on it during every pick evaluation. The exception degraded the whole
persona layer to envelope-only — silently, from the operator's view.
Reachable via persona card import or a hand-edited row.

Three legs, per the issue's proposal:

  1. Fail-safe matcher: null DaysOfWeek now means "no day gate", the
     same semantics as [] (least-astonishing reading of the shape).
  2. Read-seam normalization: PersonaTasteRepository coalesces a null
     collection after deserialize, so the domain type's non-null
     contract actually holds downstream.
  3. Never silent again: PersonaRanker WARNs (once per rule per pick)
     when a rule's evaluation throws and skips just that rule — one
     bad rule no longer faults the layer, and it can't disable
     invisibly (F82.6 observability contract).

Closes #87

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four call sites hand-copied the Artist ?? Genre ?? Tag fallback chain
(LlmPromptBuilder, Orchestrator's debug line, BoothLogFiredRuleSummary,
PersonaController's taste table) — and the gh-#87 fix was about to add
a fifth. TastePredicate.LabelOr(fallback) now owns the precedence; each
surface keeps its own documented fallback wording ("this pick" / "any" /
"any track"), which was the deliberate divergence, while the precedence
itself exists exactly once.

Closes #89

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te voting

Safe-scope content (rows whose library_id falls in the live
Station:SafeScope:LibraryIds — the seeded safe loop, authored safe
segments, station IDs) is functional audio, not rateable music. It was
votable, never-play-able, and taste-thumbable on every surface; a thumb
on a safe play even accrued an artist rule for the STATION's own name,
and a never-play write could depotentiate the never-silent fallback.

Server (authoritative, all checks against the LIVE safe scope):

  * ISafeScopeProvider — the SafeScope mirror of IStationScopeProvider,
    bound over IOptionsMonitor<StationOptions> in the Host.
  * IMediaRating: vote/never-play answer SafeContentExcluded (mapped to
    403), GetRatings stamps rateable:false, and both bulk sweeps carve
    safe libraries out of their WHERE. F33.5's main-scope exemption
    stands untouched — this narrows it for safe content only (doc
    comments updated to record the distinction).
  * BulkRatingController 403s a filter that explicitly NAMES a safe
    library — loud, not a silent updated:0.
  * booth_log.media_id (db/22 + consolidated db/06): track-start rows
    stamp the aired catalog id at publish time. station_svc has no
    grant on library.media, so the Host resolves membership through the
    new IMediaLibraryMembership seam on the library connection —
    ThumbTaste 400s a safe airing before the accrual store is reached,
    and GET /api/booth-log flags such rows tasteExcluded.

Admin UI (no-control-not-disabled, everywhere):

  * Live card + play history hide vote controls when rateable:false.
  * Catalog hides the never-play control via the new AdminMediaDto
    rateable projection.
  * Booth-log feed and the Live thumbs resolution treat tasteExcluded
    rows as unthumbable.

An empty safe scope excludes nothing — the pre-#99 behavior.

Closes #99

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@genwave-radio
genwave-radio merged commit 927f283 into main Jul 23, 2026
10 checks passed
@genwave-radio
genwave-radio deleted the fix/taste-cluster-99-87-89 branch July 23, 2026 16:41
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

1 participant