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:
- Replace it with a verified-live equivalent covering the same
Skillstags (don't just delete the entry — a role losing its only match is worse than a slightly different resource). - Re-verify with WebFetch where possible; fall back to search-index corroboration for domains that block automated fetches (LeetCode does — see above).
- 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):
- JD keywords — every keyword in
job.Fit.MatchedKeywordsandjob.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" → tagsql; "pandas"/"dataframe" → tagpandas) to catalogSkillstags. Deliberately simple substring matching, not NLP/fuzzy scoring — easy to read, easy to extend, easy to test exhaustively. - Target-role signal — the user's
IntakeProfile.Extended.DesiredTitleandTopSkills, plus the job's ownTitle, 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. - 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}/practice→PracticeRecommendation[]for that job (404 if the job doesn't exist), built from the storedJobPostingplus the currentIntakeProfile.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:
robots.txt(leetcode.com/robots.txt) explicitly listsDisallow: /graphqlalongside/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-knownmatchedUserquery used to fetch a username's public solved counts — is not meant to be crawled or queried programmatically.- 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. - Community reality check: a large number of open-source wrappers (
leetcode-queryon npm, several GitHub "unofficial LeetCode API" projects) do callleetcode.com/graphqlfor exactly thismatchedUserpublic-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 bothrobots.txtand 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/graphqlor 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).