Skip to content

WorkWingman — Learning Paths / Practice Catalog (Technical)

Extends the Study & prep feature with a curated, hand-verified catalog of practice resources — LeetCode courses and study plans, plus role-specific platforms like Databricks, Snowflake, and Kaggle Learn — matched to a job's role/tech, and an optional, carefully-scoped LeetCode-progress deep-link. Built on the same patterns as StudyPlanService: pure, local, deterministic matching against JobPosting.Fit keywords, with no network calls at recommendation time.

Part A — the practice catalog

PracticeCatalogService (src/WorkWingman.Infrastructure/Services/PracticeCatalogService.cs) implements IPracticeCatalogService (src/WorkWingman.Core/Interfaces/IPracticeCatalogService.cs).

IReadOnlyList<PracticeRecommendation> RecommendFor(JobPosting job, IntakeProfile? profile = null);

Each catalog entry is a PracticeResource (src/WorkWingman.Core/Models/PracticeResource.cs): Name, Url, Kind (Course / StudyPlan / Platform), Skills (lowercase tags — sql, pandas, system-design, dsa, javascript, data-engineering, spark, …), IsPremium, IsFree.

Catalog contents (12 entries, every URL verified live at write time)

Name Kind Skills Premium
System Design for Interviews and Beyond Course system-design Yes
Interview Crash Course: Data Structures and Algorithms Course dsa No
Top SQL 50 StudyPlan sql, data-engineering No
Advanced SQL 50 (Premium) StudyPlan sql, data-engineering Yes
30 Days of Pandas StudyPlan pandas, data-engineering No
Introduction to Pandas StudyPlan pandas, data-engineering No
30 Days of JavaScript StudyPlan javascript, frontend No
Databricks Training & Certification Platform data-engineering, spark No
Snowflake University Courses Platform data-engineering, sql, snowflake No
dbt Fundamentals (dbt Learn) Platform data-engineering, dbt, sql No
Kaggle Learn: Pandas Platform pandas, data-engineering No
Kaggle Learn: Python Platform python, data-engineering No

URL verification log

LeetCode blocks the WebFetch tool's crawler (HTTP 403 on every leetcode.com/* URL, including the study plans already shipped in StudyPlanService — e.g. leetcode-75), so LeetCode URLs were corroborated instead via independent web-search indexing (search engines have crawled and title-tagged the exact URL, confirming it's live and matches the expected content) — the same verification standard used for every other URL in this catalog:

URL Verified via Result
leetcode.com/explore/featured/card/system-design-for-interviews-and-beyond/ Search-indexed Live
leetcode.com/explore/interview/card/leetcodes-interview-crash-course-data-structures-and-algorithms/ Search-indexed (title match) Live
leetcode.com/studyplan/top-sql-50/ Search-indexed, title "SQL 50 - Study Plan - LeetCode" Live
leetcode.com/studyplan/premium-sql-50/ Search-indexed, title "Advanced SQL 50 - Study Plan - LeetCode" Live — note: LeetCode's own page title is "Advanced SQL 50", not "Premium SQL 50"; catalog entry name reflects the real title while keeping the task's "premium-sql-50" URL slug
leetcode.com/studyplan/30-days-of-pandas/ Search-indexed, title match Live
leetcode.com/studyplan/introduction-to-pandas/ Search-indexed, title match Live
leetcode.com/studyplan/30-days-of-javascript/ Search-indexed, title match Live
www.databricks.com/learn/training/ WebFetch (200, full page content read) Live
learn.snowflake.com/en/courses/ WebFetch (200, full page content read) Live
learn.getdbt.com/courses/dbt-fundamentals Search-indexed, title "dbt Fundamentals - dbt Learn" Live
www.kaggle.com/learn / www.kaggle.com/learn/pandas / www.kaggle.com/learn/python WebFetch + search-indexed Live

No URL from the task brief needed replacing — all were confirmed live. Two candidates considered and dropped in favor of the entries above:

  • freeCodeCamp JavaScript Algorithms and Data Structures — dropped. freeCodeCamp is mid-migration between a "Legacy V7", "Legacy V8", and a "Beta" curriculum at three different URLs simultaneously; no single URL is stable enough to commit to a catalog that's meant to stay correct without constant babysitting. 30 Days of JavaScript (LeetCode) covers the same skill tag with one stable URL.
  • DataCamp — dropped. Its free tier is a single preview lesson per course, not a full free study path like Kaggle Learn's; Kaggle Learn covers the same pandas/Python ground with an actually-free full course.

Catalog maintenance note

This is a hand-maintained, small catalog by design — not a scraped or auto-updated list. When a URL 404s in the future:

  1. Replace it with a verified-live equivalent covering the same Skills tags (don't just delete the entry — a role losing its only match is worse than a slightly different resource).
  2. Re-verify with WebFetch where possible; fall back to search-index corroboration for domains that block automated fetches (LeetCode does — see above).
  3. Update the entry count and table in this doc and the mirrored count assertion in PracticeCatalogServiceTests.

The matcher

RecommendFor(JobPosting job, IntakeProfile? profile = null) is pure, local, and deterministic — no I/O, no randomness, no network calls. It layers three signal sources, each adding recommendations and each de-duplicating against a running HashSet<string> of URLs already recommended (so a resource matched by multiple signals appears once):

  1. JD keywords — every keyword in job.Fit.MatchedKeywords and job.Fit.GapKeywords (gaps matter most — that's exactly where prep pays off) is mapped through a small, explicit keyword vocabulary (Vocabulary.TagWords — e.g. "sql"/"postgres"/"query" → tag sql; "pandas"/"dataframe" → tag pandas) to catalog Skills tags. Deliberately simple substring matching, not NLP/fuzzy scoring — easy to read, easy to extend, easy to test exhaustively.
  2. Target-role signal — the user's IntakeProfile.Extended.DesiredTitle and TopSkills, plus the job's own Title, run through the same vocabulary. This covers the case where the JD text itself is thin on keywords but the user's stated goal (e.g. "Data Engineer") should still steer recommendations.
  3. Always-on SWE fallback — any software-shaped role (title or role signal containing "engineer"/"developer"/"swe"/"programmer"/"architect") always gets the DS&A crash course (mirrors StudyPlanService's static seed-resource pattern, so the section is never empty for a software role); a senior-sounding title (contains "senior"/"staff"/"principal"/"lead"/"sr.") additionally gets the system-design course.

Each PracticeRecommendation carries MatchedTo explaining why it surfaced — JD: "SQL", target role: "Data Engineer", any SWE role, or senior SWE role — the same transparency pattern as StudyResource.MatchedTo.

Premium-gated entries (IsPremium = true) are surfaced with a "requires LeetCode Premium" badge in the UI, tying into the same BYO-account-link concept as StudyResource.EnrollableViaLinkedAccount — WorkWingman never bundles or resells LeetCode Premium; it only tells you a resource needs it.

API

StudyController (src/WorkWingman.Api/Controllers/StudyController.cs):

  • GET /api/study/{jobId}/practicePracticeRecommendation[] for that job (404 if the job doesn't exist), built from the stored JobPosting plus the current IntakeProfile.
  • GET /api/study/leetcode-progress-link?username=...LeetCodeProgressNote (see Part B).

Part B — LeetCode-progress tailoring: the ToS verdict

The question: is LeetCode's public profile data (solved-count-by-difficulty for a username) available through an official, sanctioned public endpoint, such that WorkWingman could read it to prioritize the catalog (e.g. "few Hards solved — start with the DS&A crash course")?

Research performed:

  1. robots.txt (leetcode.com/robots.txt) explicitly lists Disallow: /graphql alongside /api/, /submissions, /accounts, /progress, /points, and other account-adjacent paths. This is LeetCode's own machine-readable signal that the GraphQL endpoint — including the community-known matchedUser query used to fetch a username's public solved counts — is not meant to be crawled or queried programmatically.
  2. Terms of Service (leetcode.com/terms/) explicitly prohibits "crawling," "scraping," or "spidering" any part of the Service, and separately prohibits any automated process that operates without an active login session or that places undue load on the platform's infrastructure.
  3. Community reality check: a large number of open-source wrappers (leetcode-query on npm, several GitHub "unofficial LeetCode API" projects) do call leetcode.com/graphql for exactly this matchedUser public-stats query, and LeetCode has not (as far as this research found) published any official, documented, rate-limited public API for it. This is the textbook shape of a tolerated-unofficial integration: widely used, not DMCA'd into oblivion, but never affirmatively sanctioned — and directly contradicted by both robots.txt and the ToS text above.

Verdict: not clearly ToS-permissible. robots.txt disallowing /graphql plus an explicit anti-scraping ToS clause is a strong, unambiguous "no," regardless of how normalized the community workaround has become. "Lots of people do it and haven't been sued" is not the same as "the platform sanctions it," and the task's hard rule is explicit: if the endpoint is not clearly permissible, don't call it.

Path chosen: catalog-only Part A + a deep-link, never an API call. WorkWingman:

  • Never calls leetcode.com/graphql or any other LeetCode endpoint.
  • Never logs into LeetCode, stores LeetCode credentials, or reads private/authenticated data.
  • Accepts a user-typed public username (never scraped, never inferred) purely to build a URL: https://leetcode.com/u/{username}/.
  • Surfaces that URL as a "Based on your LeetCode progress" note with supportive copy — the user clicks through and reads their own public profile on leetcode.com itself. No solved-count data ever enters WorkWingman.

PracticeCatalogService.BuildProgressDeepLink(string leetCodeUsername) implements exactly this: blank/whitespace input returns a guidance message ("Add your LeetCode username…") with no URL; non-blank input is trimmed and URL-escaped into the profile link plus the supportive message. No HttpClient/Flurl call exists anywhere in this code path — it is pure string building, the same class of guarantee LeetCodeProgressNote documents on its own type.

If a future contributor wants to revisit this: the bar to clear is an official, documented LeetCode API with published rate limits and terms permitting third-party read access — not "the community endpoint still works." If that ever ships, LeetCodeProgressSource (config/opt-in, username-gated, graceful degradation, rate-limited, mocked via Flurl HttpTest in tests — zero real network in tests) is the natural next step, following the same shape as SecEdgarSource (an official, ToS-clean government API) rather than LayoffsFyiSource (a community-tolerated one this project already treats cautiously). Until then, this is intentionally the harder line.

Testing

PracticeCatalogServiceTests (xUnit, 30 tests) covers: catalog shape/count, Premium/Free flag consistency, JD-keyword matching per role (SQL, data engineering, frontend, DSA), case-insensitive role/tag matching, the always-on SWE/senior fallback and its absence for non-senior/non-SWE roles, keyword-vs-fallback dedup, gap-keyword matching, de-duplication, determinism, profile-driven matching independent of JD text, blank-title/blank-keyword edge cases, null-profile and null-job handling, and every BuildProgressDeepLink branch (valid username, URL-escaping, blank/whitespace, trimming) — with an explicit assertion that the deep-link path never calls out to the network.

frontend/src/app/features/study/study.spec.ts covers the Angular side: loading practice recommendations on init and on job switch, the empty-match state, practiceKindBadge mapping, and the LeetCode username lookup flow (blank input short-circuits, valid input sets the note, guidance-only notes render without a link).