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:
Gabriel Ignacio Mensi
2026-08-03 18:46:59 +02:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 20d863044b
commit 6392ca1628
9 changed files with 83 additions and 17 deletions
+11 -2
View File
@@ -18,7 +18,15 @@ This repo is a job application workspace. Claude acts as a career advisor and ap
### Identity
- **Name:** [YOUR_NAME]
- **Location:** [YOUR_CITY], [YOUR_COUNTRY] ([YOUR_COMMUTE_CONSTRAINTS])
- **Languages:** [YOUR_LANGUAGES]
- **Languages:**
| Language | Level |
|----------|-------|
| [LANGUAGE] | [LEVEL] |
<!-- Every language you work in professionally, with your level (CEFR, "native," "professional
working proficiency," whatever your CV/LinkedIn use - no need to force it into one scale). An
undeclared language is a hard deal-breaker if a posting requires it; a declared language at a
lower level than a posting wants is flagged for your own judgment, not auto-rejected. See
04-job-evaluation.md's Language Gate. -->
- **CV language:** [YOUR_CV_LANGUAGE] <!-- English unless your market expects otherwise; /setup asks -->
- **Status:** [YOUR_EMPLOYMENT_STATUS]
@@ -74,7 +82,8 @@ This repo is a job application workspace. Claude acts as a career advisor and ap
- [SECTOR_2]: [EXAMPLE_COMPANIES]
### Deal-breakers
<!-- Hard constraints on job search -->
<!-- Hard constraints on job search. Language requirements are handled separately and
automatically from your Languages table above - don't duplicate them here. -->
- [DEALBREAKER_1]
- [DEALBREAKER_2]