mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
Add a Language Gate - no dimension currently checks language requirements (#275)
Nothing in the framework checks a posting's language requirements
against what the candidate actually speaks. It is not one of the five
Scoring Dimensions in 04-job-evaluation.md, it is not checked in
/scrape's Step 3 fit assessment, and it is not a field in /rank's JSON
output - even though /apply's Step 1 already extracts a posting's
required language generically, with nowhere to report a mismatch to.
This adds a Language Gate, structured like the existing Eligibility
Gate (read the posting, classify against profile data, hard-stop on a
real mismatch), built on a new structured Languages table in CLAUDE.md
/ 01-candidate-profile.md. /setup now asks for it directly (Path C), or
infers it from a CV/LinkedIn export (Paths A/B - LinkedIn exports
already carry a self-rated Languages section).
The gate compares a posting's stated language requirements against
that table with three outcomes:
- Requires a language not declared at all -> hard FAIL, never
presented.
- Requires a higher level in a language that is declared (e.g. "fluent
English" against a declared B1/B2) -> FLAG, not an auto-reject -
scored and drafted normally, with the gap surfaced so the candidate
judges it themselves (a "fluent" bar reads very differently from a
strict employer vs. one that's flexible on it).
- Requires a language at or below the declared level -> clean PASS.
Wired through the three places that need it: /scrape (Step 3), /rank
(new language_gate/language_note fields alongside the existing
location veto - both are now persisted to seen_jobs.json, not just
used transiently to decide one run's shortlist), and /apply (Step 1's
language extraction now has somewhere to report to).
Out of scope, deliberately: this does not touch the free-form
Deal-breakers list or how it's used elsewhere (e.g. Scoring Dimension
4's relocation check) - that's a separate question this change takes
no position on.
Validated with two live-testing passes against real, unfetched
postings (not fabricated text) across 3 portals and 3 market languages
(Danish, German, Spanish/Argentina): 8/8 postings gated correctly in
the first pass, including ambiguous real-world wording ("you
communicate well in English") a rigid rule would have gotten wrong. A
second pass, run specifically to force a hard-FAIL case, found one
(a Danish posting requiring the ability to read Danish) and confirmed
it persists correctly and would be excluded from /rank's shortlist.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
20d863044b
commit
6392ca1628
@@ -1,5 +1,5 @@
|
||||
---
|
||||
framework_version: 1.0.0
|
||||
framework_version: 1.1.1
|
||||
---
|
||||
|
||||
# Candidate Profile
|
||||
@@ -14,10 +14,19 @@ framework_version: 1.0.0
|
||||
- **Email:** [YOUR_EMAIL]
|
||||
- **LinkedIn:** [YOUR_LINKEDIN_URL]
|
||||
- **GitHub:** [YOUR_GITHUB_URL]
|
||||
- **Languages:** [YOUR_LANGUAGES with proficiency levels]
|
||||
- **Status:** [YOUR_EMPLOYMENT_STATUS]
|
||||
- **Constraints:** [YOUR_COMMUTE_OR_LOCATION_CONSTRAINTS]
|
||||
|
||||
### Languages
|
||||
<!-- Every language you can work in professionally, with your honest level. Used by the
|
||||
Language Gate in 04-job-evaluation.md and by job-scraper/search-queries.md's query-language
|
||||
generation. Omit any language you don't actually work in - an undeclared language is treated as
|
||||
a hard no, not a gap to smooth over. -->
|
||||
|
||||
| Language | Level | Notes |
|
||||
|----------|-------|-------|
|
||||
| [LANGUAGE] | [LEVEL, e.g. "Native" / "C2" / "B1/B2 (conversational)"] | [optional] |
|
||||
|
||||
## Education
|
||||
|
||||
| Degree | Period | Institution | Key Topics |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
framework_version: 1.1.0
|
||||
framework_version: 1.2.2
|
||||
---
|
||||
|
||||
# Job Evaluation Framework
|
||||
@@ -30,6 +30,22 @@ If the candidate's permit also constrains *hours* or *start date* (a student vis
|
||||
|
||||
A role that fails this gate is not scored and not drafted. Everything below applies only to roles that pass it.
|
||||
|
||||
## Language Gate — run before scoring
|
||||
|
||||
No dimension or gate anywhere in this framework currently checks a posting's language requirements against what the candidate actually speaks - it is not one of the five Scoring Dimensions below, not a field `/scrape` or `/rank` track, and not something `/apply`'s language detection (Step 1, which already extracts a posting's required language generically) has anywhere to report to. This gate adds that check, structured the same way as the Eligibility Gate above: read the posting, classify against profile data, and treat a hard mismatch as FAIL before scoring.
|
||||
|
||||
Read the posting's language requirements as stated for **the role itself** — not the language the ad happens to be written in. A posting written in a language you don't work in, for a role that only needs languages you do work in on the job, passes fine; only an explicit job-condition requirement ("fluent X required," "must communicate with the Y team in Z") triggers this check. For each language the posting requires as a job condition, compare it against your Languages table in CLAUDE.md / `01-candidate-profile.md`:
|
||||
|
||||
| Posting requirement vs. your Languages table | Verdict |
|
||||
|---|---|
|
||||
| Requires a language **not on your table at all** (e.g. "fluent Polish required," "must communicate with the Warsaw team in Russian," and you list no Polish/Russian row) | **FAIL — hard stop.** Do not score, do not draft. Quote the exact requirement line. |
|
||||
| Requires a language you **do** list, but the posting's stated bar (as written — "fluent," "native," "C1+," "business-level") reads as plausibly **higher** than your declared level | **FLAG, then proceed.** Not a fail. Score and draft normally, but surface the gap explicitly in your report to the user (quote both the posting's requirement and your declared level) so they can judge it themselves — bars like "fluent" vary a lot by company and geography, and a recruiter may be flexible. Never silently drop the posting and never silently treat it as a clean pass. |
|
||||
| Requires a language you list, at or below your declared level (or the posting doesn't specify a level at all — just names the language) | **PASS.** No note needed. |
|
||||
|
||||
Judge the level comparison the same way you judge everything else in this framework: read both sides as written and reason about it, don't force either into a rigid scale — CEFR letters, LinkedIn-style buckets ("professional working proficiency"), and plain-English words ("conversational," "fluent," "native") all appear in the wild and don't map onto each other precisely. When genuinely unsure whether a stated bar exceeds the candidate's level, prefer FLAG over a silent PASS — the human is meant to be the tiebreaker, not the gate.
|
||||
|
||||
**Worked example:** a candidate whose Languages table lists Spanish (Native) and English (B1/B2). A posting requiring "fluent Russian" → **FAIL**, Russian isn't declared at all. A posting requiring "fluent English" → **FLAG**, English is declared but "fluent" plausibly exceeds B1/B2 — score and draft the application, but tell the candidate this posting's bar may be a stretch and let them decide. A posting requiring "conversational English" or unspecified English → **PASS**, B1/B2 clears a "conversational" bar cleanly.
|
||||
|
||||
## Scoring Dimensions
|
||||
|
||||
Evaluate each job posting against these five dimensions:
|
||||
|
||||
@@ -113,6 +113,8 @@ For each new job, do a rapid fit check (NOT the full evaluation from `04-job-eva
|
||||
- **Medium match**: Role is adjacent to your experience
|
||||
- **Low match**: Role requires significant skills you lack
|
||||
|
||||
**Language override:** before assigning a match level, check the posting against `04-job-evaluation.md`'s Language Gate (a required language you haven't declared at all in your CLAUDE.md Languages table). A required language that's entirely undeclared overrides skill fit: mark it **Low** regardless of how well the skills align, and name it in the highlight bullets so it isn't buried under an otherwise-good-looking match. A **declared** language at a requirement that reads higher than your declared level is *not* an override — score fit normally, but add a red-flag bullet under that job's highlights (Step 5) quoting the posting's requirement next to your declared level, so the gap is visible without being auto-downgraded.
|
||||
|
||||
### Step 4: Deduplicate & Store
|
||||
|
||||
1. Add ALL fetched jobs (new and skipped) to `seen_jobs.json` with structure:
|
||||
@@ -203,7 +205,7 @@ health: <portal-name> - broken (0 results for the SKILL.md test query and a broa
|
||||
|---|-----|-------|---------|----------|----------|-----|
|
||||
| 1 | High | ... | ... | ... | ... | [Link](...) |
|
||||
|
||||
If Step 2.5 flagged a mass-posting pattern, note it in the Title cell (e.g. "Frontend Developer (posted in 6 cities)") rather than burying it - it's a signal the user should see at a glance, not just in the detail highlights below.
|
||||
If Step 2.5 flagged a mass-posting pattern, note it in the Title cell (e.g. "Frontend Developer (posted in 6 cities)") rather than burying it. Do the same for a declared-language-insufficient-level flag from the Language Gate (e.g. "Backend Engineer ⚠ fluent English required") - both are signals the user should see at a glance, not just in the detail highlights below.
|
||||
|
||||
### High-Match Highlights
|
||||
For each high-match job, add 2-3 bullet points:
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
The `site:` query templates in this file are the **WebSearch fallback** — for portals without a CLI, company career pages, or when a CLI fails.
|
||||
|
||||
**Language scope:** write every query category in every language listed in your CLAUDE.md Languages table (typically 1-2, sometimes more). A posting requiring a language you have *not* declared, as a job condition, is excluded before scoring; a posting requiring a *higher level* than you declared in a language you *do* work in is flagged for your own judgment, not excluded — see `04-job-evaluation.md`'s Language Gate, the single source of truth for this rule. Translate each category's keywords rather than machine-translating word-for-word (e.g. "Frontend Developer" -> "Desarrollador Frontend", not a literal word-for-word translation) if you work in more than one language.
|
||||
|
||||
## Search Sites
|
||||
|
||||
Primary (your market's job boards - scaffold one with `/add-portal`):
|
||||
@@ -21,7 +23,7 @@ Secondary (company career pages via Google):
|
||||
|
||||
## Query Categories
|
||||
|
||||
Queries are grouped by priority. Each query should be combined with your location terms (e.g. your city, region, or metro area) where the site supports it.
|
||||
Queries are grouped by priority. Write **each category in every language from your Languages table** (see Language scope above). Combine each query with your location terms (e.g. your city, region, or metro area) where the site supports it.
|
||||
|
||||
### Priority 1: [YOUR_PRIMARY_ROLE_TYPE]
|
||||
|
||||
@@ -71,6 +73,10 @@ When evaluating results, verify the job location is within reasonable commute di
|
||||
- [BORDERLINE_AREA] (borderline - ~X min by transit)
|
||||
- [TOO_FAR_AREA] (too far)
|
||||
|
||||
## Language Filter
|
||||
|
||||
Your working languages and levels are in CLAUDE.md's Languages table. When filtering scraped results, apply `04-job-evaluation.md`'s Language Gate: a posting requiring a language you haven't declared at all is excluded; a posting requiring a higher level than you declared in a language you do work in is not excluded, flag it clearly instead (see `job-scraper/SKILL.md`'s Step 3 "Quick Fit Assessment" for how the flag surfaces in `/scrape` output). Postings simply *written* in a language you don't work in, that don't require it on the job, are fine.
|
||||
|
||||
## Date Filter
|
||||
|
||||
Only include jobs posted within the last 14 days, or with an application deadline that has not yet passed. If a posting date cannot be determined, include it but flag as "date unknown".
|
||||
|
||||
Reference in New Issue
Block a user