Skip to content

feat(career): curated genre drills — per-instrument, achievably cleared - #943

Merged
byrongamatos merged 1 commit into
mainfrom
feat/career-drill-curation
Jul 13, 2026
Merged

byrongamatos merged 1 commit into
mainfrom
feat/career-drill-curation

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Career v2, workstream 3. 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.

  • Per-instrument node lists: virtuoso_nodes: {instrument: [ids]} — a keys passport never demands a guitar drill; a flat list keeps meaning guitar (back-compat).
  • Achievable "cleared": keysCleared (a top-tier clean pass in one key — virtuoso's first gained-only artifact) now counts, alongside depth flips and mastery. The depth rungs additionally require a maxed speed tier — too high a bar for Bronze.
  • Gained-only drill merge at the intake: a completion artifact once relayed never un-earns via a stale snapshot (multi-browser races, settings import, the boot relay) — masteredAt/depth/keysCleared only grow. Incoming payload is size-bounded before the merge so junk-dropping can't bypass the 413 guard.
  • Boot relay is now once-per-session unconditionally — drill progress made before the career plugin existed can't deny a gated badge until the next virtuoso event.
  • Invite copy names the actual blocker: songs first, then "Clear Blues Shuffle in Virtuoso to mint this stamp" once the song bar is met.
  • Pre-release behavior note: v1 passports aren't in any shipped build, so no earned badge demotes in the wild.

Testing

Shipped-blues gating + keysCleared clearing, per-instrument resolution (keys earns songs-only), gained-only merge vs stale snapshots, flat-list back-compat, 413 guard preserved. 26 career tests green. Codex preflight: clean (round 5 — rounds 1–4 caught the localStorage bootstrap gap, the stale-relay clobber, the size-guard bypass, and the wrong invite copy).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Career passports now include curated genre-specific virtuoso drills.
    • Drill requirements are tailored by instrument, with clear, human-readable names.
    • Genres without a curated drill remain song-focused.
    • Passport progress can recognize cleared drills from qualifying key-clear evidence.
  • Bug Fixes

    • Drill progress now syncs automatically and preserves the highest recorded progress.
    • Stale updates no longer undo cleared drills or earned passport badges.
    • Passport guidance better reflects remaining song and drill requirements.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@byrongamatos, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c3de310-df8d-4ae7-9055-77210059bb83

📥 Commits

Reviewing files that changed from the base of the PR and between a5cfe77 and 1d014a5.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • plugins/career/passports.json
  • plugins/career/routes.py
  • plugins/career/screen.js
  • tests/plugins/career/test_passports.py
📝 Walkthrough

Walkthrough

Changes

Career passports now load curated Virtuoso drills from configuration, evaluate requirements per instrument, preserve drill progress through gained-only merges, expose drill labels, and render labeled drill requirements in the client UI. Tests cover song-only completion, drill-gated completion, and stale progress snapshots.

Career passport drill flow

Layer / File(s) Summary
Configure curated genre drills
plugins/career/passports.json, CHANGELOG.md
Genres define configured Virtuoso nodes and display labels, with documented clearing and per-instrument semantics.
Evaluate and persist drill progress
plugins/career/routes.py
Passport requirements are instrument-aware, key-cleared evidence counts toward completion, drill snapshots merge monotonically, labels are returned, and payload sizes are bounded.
Render and validate passport progress
plugins/career/screen.js, tests/plugins/career/test_passports.py
Local drill state is relayed during bootstrap, labeled drill requirements are rendered, and passport completion plus stale-snapshot behavior are tested.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlayerClient
  participant CareerPassportsAPI
  participant DrillProgressState
  PlayerClient->>CareerPassportsAPI: Relay local byNode drill snapshot
  CareerPassportsAPI->>DrillProgressState: Merge gained drill progress
  DrillProgressState-->>CareerPassportsAPI: Persisted snapshot
  CareerPassportsAPI-->>PlayerClient: Passport requirements and drill labels
  PlayerClient->>PlayerClient: Render cleared and pending drills
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely captures the main change: curated per-instrument genre drills with clearer clearing rules.
Description check ✅ Passed The description covers the core changes and testing, though it doesn't strictly follow the template's sections or checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/career-drill-curation

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
plugins/career/routes.py (1)

303-346: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unfiltered, never-shrinking byNode merge can eventually brick future drill-state updates.

_merge_drill_nodes persists every node id relayed in byNode, not just the handful actually referenced by curated virtuoso_nodes in passports.json, and the merge never shrinks (gained-only by design). If the relayed virtuoso.progress.byNode snapshot reflects virtuoso's whole node catalog (likely much larger than the 5 curated drills here) rather than just career-relevant nodes, the persisted snapshot grows monotonically over the life of an install. Once it nears DRILL_SNAPSHOT_MAX_BYTES, every subsequent /drill-state POST — including ones carrying real progress on a still-required drill — gets rejected with 413 forever, since there's no pruning path.

Consider filtering both the incoming and persisted byNode down to the node ids actually referenced by any genre's virtuoso_nodes before merging/persisting, so storage stays bounded by curated content rather than virtuoso's full catalog.

♻️ Proposed fix to bound persisted byNode to curated node ids
+def _known_drill_nodes():
+    cfg = _state["passports_content"]
+    known = set()
+    for genre_cfg in (cfg.get("genres") or {}).values():
+        nodes = (genre_cfg or {}).get("virtuoso_nodes") or []
+        if isinstance(nodes, dict):
+            for lst in nodes.values():
+                known.update(n for n in (lst or []) if isinstance(n, str))
+        else:
+            known.update(n for n in nodes if isinstance(n, str))
+    return known
+
 def _merge_drill_nodes(old, new):
     out = dict(old)
-    for node_id, incoming in new.items():
+    known = _known_drill_nodes()
+    for node_id, incoming in new.items():
+        if node_id not in known:
+            continue
         if not isinstance(incoming, dict):
             continue

To confirm the risk profile, it would help to know the typical size of virtuoso's full byNode catalog relative to DRILL_SNAPSHOT_MAX_BYTES.

Also applies to: 576-585

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/career/routes.py` around lines 303 - 346, Update the drill-state
persistence flow around _merge_drill_nodes to restrict both the incoming and
existing byNode maps to node IDs referenced by any genre’s virtuoso_nodes
configuration. Merge and persist only this curated subset, while preserving the
gained-only behavior for retained nodes so real progress on required drills
continues to be accepted and storage remains bounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@plugins/career/routes.py`:
- Around line 303-346: Update the drill-state persistence flow around
_merge_drill_nodes to restrict both the incoming and existing byNode maps to
node IDs referenced by any genre’s virtuoso_nodes configuration. Merge and
persist only this curated subset, while preserving the gained-only behavior for
retained nodes so real progress on required drills continues to be accepted and
storage remains bounded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a289c328-2f8c-4ee1-8bc3-be105f84ce53

📥 Commits

Reviewing files that changed from the base of the PR and between 45caa86 and a5cfe77.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • plugins/career/passports.json
  • plugins/career/routes.py
  • plugins/career/screen.js
  • tests/plugins/career/test_passports.py

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>
@byrongamatos
byrongamatos force-pushed the feat/career-drill-curation branch from a5cfe77 to 1d014a5 Compare July 13, 2026 13:14
@byrongamatos
byrongamatos merged commit 4027c31 into main Jul 13, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant