* 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>
* feat(career): gigs backend — propose a setlist, log the completed set
Career v3, WS3 (backend half). A gig is career's verb:
- POST /gigs/propose {instrument, genre, size}: setlist from the
passport's own stubs — qualifying songs (per the genre's badge bar,
family-aware) shuffled for a free re-roll, topped with the
highest-accuracy near-bar songs as stakes, and filled from UNPLAYED
genre songs when the passport is young (the first gig is how stubs
start). Names the highest venue the current stars can book.
- POST /gigs: logs a COMPLETED set only (abandoned sets never log — no
fail state). Per-song accuracy = MAX(last_accuracy) from song_stats,
freshly written by the set's own plays; encore = avg ≥ the data-driven
bar (passports.json gig.encore_accuracy, 0.75). Appends to the career
state file (same atomic _save_json pattern).
- Passports view: per-passport gigs (newest first, capped 20) and
per-instrument gig_count — the profile wall's gig line lights up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(career): gig backfill offsets by qualifying taken, not picks length
CodeRabbit on #954: after the stakes loop appends near-bar songs,
qualifying[len(picks):] overshoots and skips eligible qualifying songs
— a stocked passport could still get a short set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(career): practice invitations — closest stamps + bring-these-up
Career v3, WS1. The passport now points at the practice that pays:
- Stubs carry next_star_at (the same primitive _stars() uses) and each
passport exposes `nearest`: the top 3 non-qualifying songs by distance
to their next star, in the worklist order.
- "Closest stamps" strip above the shelf: the in-progress graded
passports nearest to minting, each row naming the ask — N more songs
(with the nearest title + best %) or the blocking Virtuoso drill.
Rows open the passport.
- "Bring these up" list on the stubs page of in-progress passports;
earned pages stay memorabilia (no homework on a won badge).
- Invitation-voiced throughout: no meters, no completion pressure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(test): comment says qualifying-bar ranking, matching the assertion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The enrichment fallback made the passport rack real (hundreds of MB
sub-genres) but only the five exact umbrella keys carried Virtuoso
drills. Genres now resolve to a family by keyword substring (MB's
vocabulary is open — 'metalcore' must hit metal without an alias),
first-match-wins in list order ('blues rock' → blues), and inherit the
family's requirement from the same genres map. Exact entries still win;
per-instrument scoping unchanged; unmatched genres stay songs-only.
Data: families for metal (incl. djent/grindcore/thrash/doom), blues,
jazz (bebop/swing/bossa), funk (disco), rock (punk/grunge/shoegaze).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Career v2, WS3. Bronze in blues/rock/metal/funk/jazz now also asks for
the genre's signature Virtuoso drill, data-driven in passports.json:
blues_shuffle, rock_power_backbeat, melodic_metal_gallop,
sixteenth_pocket, vl_shells — with career-side display labels the
passport page renders instead of raw node ids.
- virtuoso_nodes becomes {instrument: [node_ids]} so a keys passport
never demands a guitar drill; a flat list keeps meaning guitar
(virtuoso's content is guitar-first).
- _node_cleared also accepts keysCleared (a top-tier clean pass in one
key — virtuoso's FIRST gained-only artifact). The depth rungs
additionally require a maxed speed tier, too high a bar for Bronze.
- Genres without a curated entry stay songs-only.
Note: pre-release behavior change — v1 passports aren't in any shipped
build, so no earned badge can demote in the wild.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Career v2, WS2. Nothing measured play time before (the achievements
plugin's final-position shortcut double-counts loops and mis-reads
seeks). Now:
- stats-recorder.js accrues WALL-CLOCK seconds across song:play/resume ↔
pause/stop/ended spans (single spans clamp at 2h against suspend
inflation) and piggybacks them as `seconds` on the POSTs it already
sends; failed POSTs restore the accumulator; a session reset flushes
first so time can't re-attribute to the next song/arrangement.
- POST /api/stats accepts optional `seconds` (finite, 0 < s ≤ 6h) on the
scored and position branches, plus a new seconds-only branch for
unscored plays that ran to the natural end — banks time WITHOUT
touching the resume position (song:ended must not overwrite Continue)
and still counts as playing today for the streak.
- song_stats gains additive idempotent `seconds_total`; record_session/
touch_position accrue, new add_play_seconds() for the seconds-only
path; the legacy-encoding stats merge sums seconds across duplicates.
- Passports surface it: "14.2 h in Blues" under the badge stamp and on
the shelf cover sub-line — a true fact that only grows, never a
target or a meter (Stage 5 post-cap, per the career design).
Tests: seconds accrual/validation/seconds-only branch (stats API),
per-instrument-and-genre summing (career), fmtHours formatting (vm).
Full suites: pytest 2480, JS 1165.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The badge-journey layer on top of career stars (Christian's career-mode
v2 design, composed with the shipped venue system). Badges are computed
on read from song_stats × the library's effective genre — never stored:
Bronze = N genre songs at min_stars (data-driven in passports.json,
default 5 songs at 2★) plus any configured virtuoso drill nodes.
New endpoints under /api/plugins/career/:
- GET /passports passport walls per instrument: badges, ticket
stubs (qualifying songs), library genres, drills
- POST /passports/commit instrument commitment (idempotent wax seal)
- POST /passports/open open a genre passport (implies commitment)
- POST /drill-state intake for the relayed virtuoso.progress
snapshot (career's frontend listens on the bus)
Instrument attribution reuses progression.instrument_for_arrangement via
the song_stats arrangement index; the genre column goes through the
host's override-aware effective-genre SQL. Non-graded instruments (bass,
drums) render shown-not-judged — repertoire, never a false badge denial.
Persisted state (commitments, opened passports, drill snapshot) lives
under CONFIG_DIR/career/ and rides the settings export bundle via
settings.server_files; a minimal settings.html documents it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>