Boards (signal-weighted, ~150/sweep): - Add TimesJobs (shahidirfan) — richest board: skills+salary+experience+offsite. Build/normalize + register; legacy city alias (New Delhi→Delhi, Bengaluru→Bangalore, Gurugram→Gurgaon) — verified live it returns 0 for "New Delhi", 35 for "Delhi". - Swap LinkedIn harvestapi → curious_coder (structured industry/jobFunction/applicants); harvestapi kept registered as linkedin_v1 fallback. - Drop Indeed from enabled (0% skills, redundant). Budgets: Foundit 40, TimesJobs 35, Naukri 30, LinkedIn 30, WorkIndia 15. Pool — cursorless freshness + safety: - REMOVE pagination/cursors: curious_coder only honors start=0 (start>0 → empty), proven; the rest are date feeds. Freshness now = boards' date-sort + the per-user seen-net + pool_save id-dedup. - Exhaustion guard: a refill adding < POOL_MIN_NEW_PER_REFILL new jobs flags the (user,query) exhausted → the gate relaxes the floor instead of block-fetching dupes (makes the 100/90 floor safe on niche queries). - Storage cap: pool_save trims beyond POOL_MAX_PER_QUERY freshest (kills DB swell). 72h TTL verified. - Thresholds 100/90 (deep pool, pay-for-volume). Scoring / cards: - Evidence-based fallback prose: when Haiku's cards stage gives nothing, the card uses Opus's REAL dimension notes (not a generic "Strong on X") + coverage logging + generic salvage parser. - "Skills & requirements" → "Skills fit" (consistent dimension labels). Bug fixes: - mark_seen dedups ids (was CardinalityViolationError on a duplicate id in one batch). Tests: deep stack contracts + committed board-sample fixture (no cache-pollution flakiness) + offline e2e + opt-in live e2e. 64 pass.
67 lines
4.0 KiB
Python
67 lines
4.0 KiB
Python
"""The match rubric — the ONE published source of truth for how a fit score is computed.
|
|
|
|
Integrity rule: the displayed score is NEVER emitted by a model. The curator (Opus) assesses each
|
|
DIMENSION 0-100 against a fixed ANCHOR (observable facts, not vibes); `aggregate()` combines them with
|
|
FIXED weights into the overall score. So a 94 is *earned* — role 100, location 100, skills 88, … — and
|
|
every score decomposes + audits. There is no prompt knob that lifts the number without the evidence.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
# key · label (shown in the breakdown) · weight · the ANCHOR the curator scores against. Weights sum to 1.
|
|
DIMENSIONS = [
|
|
{"key": "role", "label": "Role fit", "weight": 0.25,
|
|
"anchor": "100 = same function · 75 = adjacent function · 45 = different function · 15 = unrelated"},
|
|
{"key": "skills", "label": "Skills fit", "weight": 0.25,
|
|
"anchor": "share of the job's stated requirements the candidate genuinely meets (100 = all · 50 = half · 0 = none)"},
|
|
{"key": "seniority", "label": "Seniority fit", "weight": 0.15,
|
|
"anchor": "100 = same level · 70 = one band off · 35 = two+ bands off"},
|
|
{"key": "location", "label": "Location fit", "weight": 0.15,
|
|
"anchor": "100 = target city or remote-ok · 60 = same metro/region · 25 = different city"},
|
|
{"key": "industry", "label": "Industry fit", "weight": 0.10,
|
|
"anchor": "100 = same domain · 65 = adjacent domain · 30 = unrelated domain (omit if no industry preference)"},
|
|
{"key": "experience", "label": "Experience fit", "weight": 0.10,
|
|
"anchor": "100 = years + trajectory align · 60 = slightly under/over · 30 = large gap"},
|
|
]
|
|
|
|
KEYS = [d["key"] for d in DIMENSIONS]
|
|
WEIGHTS = {d["key"]: d["weight"] for d in DIMENSIONS}
|
|
LABELS = {d["key"]: d["label"] for d in DIMENSIONS}
|
|
|
|
|
|
def aggregate(scores: dict) -> int:
|
|
"""{dimension_key: 0-100} → the weighted overall (0-100). Only dimensions actually scored count; a
|
|
genuinely-absent one (e.g. industry with no preference) drops out and its weight redistributes over
|
|
the rest — floor-free: never a silent zero that tanks the score, never a constant that props it up.
|
|
The five core dimensions (role/skills/seniority/location/experience) always apply, so this can't be
|
|
gamed by omitting the hard ones — the curator is required to score every applicable dimension."""
|
|
present = {k: max(0.0, min(100.0, float(scores[k]))) for k in KEYS if scores.get(k) is not None}
|
|
if not present:
|
|
return 0
|
|
wsum = sum(WEIGHTS[k] for k in present)
|
|
return round(sum(WEIGHTS[k] * v for k, v in present.items()) / wsum)
|
|
|
|
|
|
def calibrate(raw: float, *, floor: float = 50.0, gamma: float = 1.3) -> int:
|
|
"""Transparent presentation calibration of the HEADLINE score (the per-dimension breakdown stays raw
|
|
evidence). A match% is a calibrated product judgment, not a physical measurement — so we apply ONE
|
|
documented curve, anchored at the floor and gently expanded at the top, so a genuinely-strong match
|
|
presents in the 90s. Guardrails that keep it honest:
|
|
• MONOTONIC — strictly increasing, so a better job ALWAYS scores higher (ordering never lies).
|
|
• FLOOR-ANCHORED — a weak match at the floor is unchanged; weak NEVER becomes strong.
|
|
• UNIFORM — the same curve for every job (no cherry-picking).
|
|
Example (gamma 1.3): 50→50, 60→63, 70→74, 80→85, 90→94, 95→97. Raw stays as-is below the floor."""
|
|
if raw <= floor:
|
|
return round(raw)
|
|
span = 100.0 - floor
|
|
return round(100.0 - span * ((100.0 - raw) / span) ** gamma)
|
|
|
|
|
|
def level_for(score: float) -> str:
|
|
"""Strong / Solid / Light band for a dimension (drives the breakdown chips)."""
|
|
return "Strong" if score >= 72 else ("Solid" if score >= 52 else "Light")
|
|
|
|
|
|
def prompt_block() -> str:
|
|
"""The rubric rendered for the curator prompt: the exact dimensions to score, with their anchors."""
|
|
return "\n".join(f' - "{d["key"]}": {d["label"]} — {d["anchor"]}' for d in DIMENSIONS)
|