Career v3: Gold tier — verified improv upgrades an earned badge (#960)

* feat(career): Gold tier — a family-style goldImprov artifact upgrades an earned badge

The drill-state relay's goldImprov map (virtuoso gold_improv mints,
gained-only merged like drill nodes) turns an earned badge gold when the
passport's genre — or its genre family — has a verified improv artifact.
Gold never substitutes for the badge bar: gold-without-bronze stays
in_progress.

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

* feat(career): Gold tier frontend — relay, ceremony, slam, gold ink everywhere

The drill-state relay now carries virtuoso's goldImprov map; a badge that
comes back gold gets its own ceremony + notification (tier-suffixed seen
ids — the bronze moment stays seen under its legacy id, a gold slam marks
both), a gold stamp slam in the book, gold ink on the shelf-cover mini
stamp, and the real gold foil chip. The bronze page's dashed 'Gold rung
coming' preview becomes a live invitation to jam the style in Virtuoso.

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

* fix(career): gold review fixes — family-space style matching, intake guards, rail counter

The review's showstopper: virtuoso mints goldImprov under raw
STYLE_PALETTES ids ('punk', 'djent', 'disco'), which are mostly NOT
family keys — the tier check now matches in family space (artifact style
and passport genre bucket through the same _genre_family keyword match),
so a 'punk' gold reaches a 'punk rock' passport. Also: non-dict
goldImprov 400s loudly instead of silently dropping; evidence-free
artifacts (no verifier) never mint; goldImprov gets the same pre-merge
size bound byNode has (junk under the cap could otherwise persist
forever and wedge every later relay at the post-merge check); the
instrument-rail badge counter counts gold (earning gold no longer made a
badge vanish from the rail); first-artifact-wins is now asserted against
the persisted snapshot instead of vacuously.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-14 11:26:36 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 0d35228d56
commit be473dc7af
7 changed files with 225 additions and 32 deletions
+49
View File
@@ -198,3 +198,52 @@ test('gig runner lifecycle: advance on ended, abandon on dead-queue stop', () =>
t.onGigSongStop();
assert.equal(t.getGigRun(), null);
});
test('a gold upgrade notifies even when the bronze moment was already seen', () => {
// Bronze seen under the legacy un-suffixed id; the badge then turns gold.
const w = load({ 'feedBack-career-badges-seen': '{"guitar/blues":1}' });
const t = w.__careerPassportTest;
const view = { instruments: { guitar: { passports: [
{ genre_key: 'blues', genre: 'Blues', badge: 'gold' }] } } };
t.detectNewBadges(view);
assert.equal(w.notifications.length, 1);
assert.match(w.notifications[0].title, /Gold/);
// Same session: no duplicate.
t.detectNewBadges(view);
assert.equal(w.notifications.length, 1);
// Gold slam seen → fresh session stays silent.
t.markBadgeSeen('guitar', 'blues', 'gold');
const w2 = load({ 'feedBack-career-badges-seen': JSON.stringify(t.seenBadges()) });
w2.__careerPassportTest.detectNewBadges(view);
assert.equal(w2.notifications.length, 0);
});
test('a gold slam marks the bronze moment seen too — never both ceremonies', () => {
const w = load();
const t = w.__careerPassportTest;
t.markBadgeSeen('guitar', 'blues', 'gold');
const seen = JSON.parse(JSON.stringify(t.seenBadges()));
assert.equal(seen['guitar/blues@gold'], 1);
assert.equal(seen['guitar/blues'], 1);
// A later view where the badge reads 'earned' (e.g. gold state lost
// server-side) must not replay the bronze ceremony.
const view = { instruments: { guitar: { passports: [
{ genre_key: 'blues', genre: 'Blues', badge: 'earned' }] } } };
const w2 = load({ 'feedBack-career-badges-seen': JSON.stringify(seen) });
w2.__careerPassportTest.detectNewBadges(view);
assert.equal(w2.notifications.length, 0);
});
test('careerTotals counts gold badges on the wall', () => {
const t = load().__careerPassportTest;
t.setView({
config: { instruments: ['guitar'] },
instruments: { guitar: { committed_at: 1, gig_count: 0, passports: [
{ genre_key: 'blues', genre: 'Blues', badge: 'gold', seconds_total: 60 },
{ genre_key: 'funk', genre: 'Funk', badge: 'in_progress', seconds_total: 0 },
] } },
});
const totals = t.careerTotals();
assert.equal(totals.badges, 1);
assert.equal(totals.walls[0].earned[0].badge, 'gold');
});