feat(discover): explain the taste match on both clients — #2377 (clients)
test-web / test (push) Successful in 33s
android / Build + lint + test (push) Successful in 3m57s

"Matches your taste in shoegaze and dream pop." replaces the seed
attribution when the candidate's own tags overlap the taste profile.

The preference order is the point of slice 6: the tag reason describes the
MUSIC ("sounds like what you like"), while seed attribution describes the
graph ("adjacent to something you played"). When we can say the former, it
is strictly the better explanation. When we can't — the common case, since
tag coverage for out-of-library artists is partial by nature (#2376) — the
card falls back to attribution rather than going blank.

Both clients share the wording, Oxford comma included, and both have tests
asserting the exact strings. That's deliberate: identical copy across two
codebases silently diverges unless something fails when it does.

Android caps at 3 tags client-side even though the server already does.
The server contract could widen; a run-on subtitle shouldn't be how we
find out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:51:27 -04:00
co-authored by Claude Opus 5
parent ca4832e620
commit eec59193fa
8 changed files with 185 additions and 2 deletions
+15 -1
View File
@@ -44,6 +44,20 @@
return next;
}
// "Matches your taste in shoegaze and dream pop." — the reason line when the
// candidate's own tags overlap the taste profile (#2377). Preferred over seed
// attribution because it describes the MUSIC ("sounds like what you like")
// rather than the graph ("adjacent to something you played"), which is the
// whole point of slice 6. Falls back when there are no matched tags, which is
// the common case: coverage is partial by nature (#2376).
function reasonText(s: ArtistSuggestion): string {
const tags = s.matched_tags ?? [];
if (tags.length === 0) return attributionText(s.attribution);
if (tags.length === 1) return `Matches your taste in ${tags[0]}.`;
if (tags.length === 2) return `Matches your taste in ${tags[0]} and ${tags[1]}.`;
return `Matches your taste in ${tags[0]}, ${tags[1]}, and ${tags[2]}.`;
}
function attributionText(attribution: SeedContribution[]): string {
if (attribution.length === 0) return '';
const verb = (s: SeedContribution) => (s.is_liked ? 'liked' : 'played');
@@ -151,7 +165,7 @@
title={s.name}
imageUrl={s.image_url}
state={cardState(s)}
attribution={attributionText(s.attribution)}
attribution={reasonText(s)}
onRequest={() => onRequest(s)}
onSnooze={() => onSnooze(s)}
onUnsnooze={() => onUnsnooze(s.mbid, s.name)}