fix: taste-integrity cluster — safe-content exclusion, null-DaysOfWeek NRE, label precedence (gh-#99, #87, #89) - #100
Merged
Conversation
…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>
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.
Summary
Station:SafeScope:LibraryIds; an empty safe scope excludes nothing (pre-Safe loop/Station Id must never allow ranking/taste voting #99 behavior).TasteMatcherno longer NREs on a{}taste context: nullDaysOfWeeknow means "no day gate", the repository read seam normalizes it to[], and a rule whose evaluation throws gets a WARN + per-rule skip instead of silently degrading the whole persona layer.Artist ?? Genre ?? Tagchains now live once asTastePredicate.LabelOr(fallback); each surface keeps its documented fallback wording.booth_log.media_id(db/22 + consolidated db/06) stamped at publish time;ISafeScopeProvider+IMediaLibraryMembershipseams (station role has no grant onlibrary.media, so the Host bridges membership on the library connection).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
dotnet build GenWave.sln— 0 warningsdotnet test GenWave.sln— full suite green (includes newGh099_*controller + Postgres specs and the gh-orchestration: TasteMatcher NREs on null DaysOfWeek — rule silently never fires #87 matcher/ranker/repository pins)cd admin-ui && npx jest— 58 suites green (new gh-Safe loop/Station Id must never allow ranking/taste voting #99 no-thumb-on-safe-row fact)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 viagit revertof the three commits.Closes #99
Closes #87
Closes #89
🤖 Generated with Claude Code