Files
matchmaking-v2/research/docs/ENGINE_DESIGN.md
raulgupta 89ca9ad647 Initial commit: matchmaking-v2 on-demand multi-board job-search agent
On-demand Scout service (replaces the nightly aggregator):
- FastAPI agent-mesh service; card name "matchmaking-service" (HTTP /a2a/tasks
  + Redis-stream worker matching the sibling-service pattern)
- run_search: parallel multi-board sweep (Naukri/blackfalcondata + Foundit +
  LinkedIn), city-filtered at the board, cheapest-per-result first
- per-board adapters + normalizers -> ScoutJob with rich read-only details and
  offsite apply links; recall mode + MVQ guard
- result cache for dev replay ($0); per-board cost knobs; retry-on-5xx
- research/: engine design, board cost economics, India-actor shortlist, POC
2026-06-18 19:22:03 +05:30

378 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Matchmaking v2 (Scout) — Engine Design & Locked Plan
> Canonical design doc. Captures the engine research, the locked decisions, and the
> open items. Written 2026-06-12. **Revised 2026-06-16** with live-evidence findings.
> Supersedes the dead `matchmaking` service.
>
> **Companion docs (gathered evidence — read alongside):**
> - `SIGNAL_AUDIT_V2.md` — evidence-based signal inventory; **supersedes §2/§5 below**.
> - `AUTOAPPLY_STATE.md` — validated browser-use auto-apply engine; **supersedes/extends §6**.
> - `DECOMMISSION_PLAN.md` — how v2 slots in over the old matchmaking (port `:8006`).
>
> **2026-06-16 deltas in one line:** the engine is richer mostly by *wiring* (interview/roleplay/
> assessment are LIVE data, not "gaps"); a new high-trust signal (**course watch-behavior**) was
> found; auto-apply is now a **validated board-agnostic vision agent** (not just hosted-submit/
> handoff); v2 reuses port **`:8006`** (not `:8011`); **Pathways is cut**.
---
## 0. Why v2 exists
The old `matchmaking` service is being decommissioned. Diagnosis (verified June 1112):
- **Wrong shape:** it was a nightly *job aggregator* — scraped ~13.8k jobs into its own
Postgres and scored 21 users against the stale pile. 986,770 match-score rows for 21 users.
- **Dead scheduling:** not cron — a self-rescheduling RQ chain in Redis that went empty
(likely an ElastiCache eviction) and silently stopped. Ingestion dead since May 27.
- **Weak engine:** ranked on the *lowest-trust* signals (self-declared prefs + claimed skills
+ an often-absent QScore that, at 30% weight, collapsed everyone to a neutral 60).
v2 is a **fresh, purely on-demand** service. No aggregation. Fetch live listings when the
user searches, score only those.
---
## 1. Locked architecture (the shape)
- **On-demand only.** Per user search, fetch ~25200 live listings from **LinkedIn + Naukri +
Indeed** via Apify, score only those. No corpus, no nightly batch.
- Sync fetch is viable: Apify `run-sync-get-dataset-items` (300s cap), cold start ~1.2s.
- 3-board search ≈ **$0.150.25 uncached**; shared cache keyed `role+location`, TTL 16h.
- **Vendor picks — VALIDATED 2026-06-16 (live probe, `poc/browser-apply/apify_probe.py`):**
- **Direct-ATS `bovi/greenhouse-lever-ashby-job-scraper` = the autonomous backbone** — public
JSON endpoints (no auth), returns clean ATS `apply_url`, **100% Tier-1 offsite** in-sample,
~$1.50/1k. Company/preset-driven (not free-text). This is the new, highest-yield inbound.
- LinkedIn `harvestapi/linkedin-job-search`**60% offsite** (`applyMethod.companyApplyUrl`),
real ATS diversity. Needs a one-time `approvePermissions` (full-account-access actor).
- Indeed `misceres/indeed-scraper` — ~35% external, but ~0% clean-ATS for India/ML (breadth, low
autonomous yield).
- Naukri → use **`muhammetakkurtt/naukri-job-scraper`** (11k users, no permission gate), NOT
`memo23` or the fishy `infinity_and_beyond`. Its `applyRedirectUrl` is the true external link,
but ~93% point to bespoke small-company career pages, not clean ATS.
- **Measured combined yield:** ~43% have an offsite link, **~14% immediately Tier-1 autonomous**
on a free-text search (more if you lead with direct-ATS). Full data → `apify-inbound` memory.
- **Surface:** ONE responsive web app, **no extension, no native app download.**
- The cross-origin limitation is now **solved a third way (validated):** a server-side/cloud
**AI browser agent (browser-use) fills any board's form by vision** — no extension, no ATS
creds needed. See §6 + `AUTOAPPLY_STATE.md`. (Hosted-submit/prepared-handoff remain fallbacks.)
- **Slots in over the OLD matchmaking — port `:8006`, not a new `:8011`** (revised; see
`DECOMMISSION_PLAN.md`). v2 inherits the existing `api.growqr.ai/matchmaking/*` route + the
frontend's matchmaking wiring. Still FastAPI in the `course-service` mold: Redis-Streams worker,
registered in orchestrator `AGENT_URLS`, reached via the orchestrator WS, frontend talks to it
through the existing `useAgentSession(...)` hook. The old matchmaking's own Postgres (corpus) is
dropped — v2 is on-demand, no corpus.
---
## 1.1 Apify call contracts (verified live 2026-06-17)
Probed each actor's real input schema (`poc/browser-apply/apify_probe.py`, key loaded). Field-level
contract + the ScoutPrefs→params coverage matrix live in **`ENGINE_INPUTS.md`**. Headlines:
- **MVQ (Minimum Viable Query) = title + a resolvable location.** No MVQ → **do not call Apify**
(engine returns `{needs:[...]}`; frontend also gates "Find my matches"). Cache key = `title+geo`, TTL 16h.
- **Free-text lane:** `harvestapi/linkedin-job-search` (`jobTitles[]`,`locations[]`), `misceres/indeed-scraper`
(`position`,`location`,`country`), `muhammetakkurtt/naukri-job-scraper` (`keyword`,`cities[]`,`maxJobs*`).
- **Curated lane (separate):** `bovi/greenhouse-lever-ashby` is **company-list-driven**; user inputs only
*filter within* it (`titleKeyword`,`locationKeyword`,`remoteOnly`,`keywords[]`).
- **Naukri is the richest target for India** — uniquely exposes `department`, `industry`, ₹ `salaryRange`,
numeric `experience`. Lead with it. **Gotcha:** Naukri `industry`/`department`/`cities` take **numeric
codes**, not labels (labels are display-only).
## 1.2 Input coercion layer (the board adapters)
Nothing from the UI goes straight to an actor — a per-board **adapter** parses/maps/converts each
`ScoutPrefs` field to that actor's exact schema. **Code: `engine/board_adapters/`** (`coerce.py` +
`build_{naukri,linkedin,indeed,ats}_input(prefs)`).
**6 coercion dimensions:** ① parse/split (`"City · Country"``{city,country,remote}`) · ② enum-map
(label→board enum) · ③ unit-convert (₹→lakhs bucket; seniority⇄years) · ④ id-lookup (name→code:
LinkedIn `geoIds`/`industryIds`, **Naukri numeric codes**) · ⑤ cardinality (multi-select→array where
supported, else **fan-out N searches + dedup** — Indeed/bovi take single strings) · ⑥ presence
(`Any`→omit, `Remote`→flag, `Anywhere`→geo-only).
**Verified lookup maps (built into `coerce.py`):**
| Map | Examples |
|---|---|
| `COUNTRY_ISO2` (Indeed) | India→`IN`, US→`US`, UK→`GB` |
| `WORKMODE_*` | Remote→`remote`, On-site→`office`, Any→omit |
| `SENIORITY_YEARS` (Naukri `experience`) | Fresher 0 · Junior 2 · Mid 5 · Senior 9 · Lead 13 |
| `SENIORITY_LINKEDIN` (`experienceLevel`) | Junior→`associate`, Mid→`mid-senior`, Senior→`director` |
| `COMP_NAUKRI_SALARY` (`salaryRange`) | ₹1622L→`15to25`, ₹2230L→`25to50` |
| `ROLE_NAUKRI_DEPT` (codes) | Product→`10` · Engineering→`5` · Design→`15` · Data→`3` · Sales→`14` · Ops→`2` |
| `INDUSTRY_NAUKRI` (codes) | Fintech→`114` · SaaS→`110` · E-commerce→`108` · EdTech→`133` · HealthTech→`131` · Consulting→`126` |
| `CITY_NAUKRI` (codes, 1→N) | Bengaluru→`[97]` · Mumbai→`[134]` · **Delhi NCR→`[6,73,220,350]`** · Hyderabad→`[17]` · Pune→`[139]` |
| `INDUSTRY_LINKEDIN_ID` | best-effort — **TODO verify** vs LinkedIn taxonomy; falls back to keyword fold |
**System-set (not user):** `maxJobs`/`maxItems` (25200 budget) · freshness (`freshness=7`/`postedLimit=week`/
`recentWindowDays=30` — the liveness gate) · `sortBy=date` · `fetchDetails`/`followApplyRedirects=true`
(real offsite apply URL) · Naukri country implicit `IN`.
**Composite-keyword fallback** for boards lacking `department`/`industry` (LinkedIn/Indeed/bovi):
`keyword = [seniorityPrefix] + title + [industryTerm]` (e.g. Indeed `position="Senior Payments PM fintech"`),
while Naukri stays clean (`keyword="Payments PM"` + structured `department/industry/salaryRange`).
**Hard rule — degrade, don't break:** any value that doesn't resolve to a board enum/code is **never
sent as an invalid enum**. It falls back to the **keyword fold** or **post-fetch filter**, and is logged.
A missing lookup loses precision, never the call.
**Engine-side-only inputs** (no board param on any actor — used for ranking/floors/exclusions/ordering):
`priorities`, `stretch`, `intent`, `availability`, `dealBreakers`, `sort`. See `ENGINE_INPUTS.md` §6.
---
## 2. Signals — keep / discard (the input audit)
> **➜ `SIGNAL_AUDIT_V2.md` supersedes this section** (evidence-based, from live-DB sampling
> 2026-06-16). Deltas: (a) **course watch-behavior** is a NEW high-trust booster — revealed
> preference ("completes React content → step toward frontend") beats stated; (b) interview/
> roleplay/assessment are **LIVE data, not gaps** (§5) but are **already digested into QScore**, so
> v1 uses the QScore quotient as the competence proxy to avoid double-counting; (c) **Pathways is
> CUT** (feature *and* its questionnaire-as-signal — weak link: broken skill-gap, impractical roles).
> The trust⟂coverage discipline below is unchanged and is what keeps "more signals" from re-causing
> the v1 collapse.
**Governing tension: trust ⟂ coverage.** High-trust signals have low coverage; high-coverage
signals have low trust. **Design rule: a signal earns SCORE WEIGHT only if its coverage is high
enough that absence isn't the norm; otherwise it is an ADDITIVE BOOSTER (bonus when present,
never a penalty when absent).** The old engine broke this rule — that's the v1 collapse bug.
### KEEP
**Tier A — backbone (high coverage; filter + light score)**
- `years_experience` (date-derived, **not** the self-declared band)
- `current_role` / title (prefer social-branding LinkedIn-verified)
- `preferred_locations` + work mode (hard filter)
- `target_roles` / `target_industries` (filter, not score — it's *want* not *is*)
- skills (normalized — see §3.2)
**Tier B — verified enrichers (medium coverage; real weight when present)**
- verified title / company / **industry** (social-branding; industry only via BrightData path)
- degree / field_of_study / institution
- certifications
- assessment scores (`passed` / `percentage` — user-service, shipped Jun 11; earned, grows)
- QScore **quotient vector** (10-D; enricher/tiebreaker — not additive-only, coverage-penalized)
**Tier C — premium boosters (low coverage; ADDITIVE ONLY, never penalize absence)**
- interview 8-D rubric (~510% coverage)
- roleplay 6-D rubric (<3%)
- improvement velocity / coachability
- video on-camera score (<1%)
- (Doubles as an adoption flywheel: "do a mock interview sharper matches.")
### DISCARD (noise for matching)
- **ATS `overall_score`** measures resume *quality/formatting*, **not job fit** the trap.
- parser confidence, `resume_check` signals (QA, not the person)
- follower/connection counts, post engagement (vanity)
- `brand_score`, `pathway_archetype` (rare/external/unpopulated)
- self-declared `salary_range` (zeros/guesses use the listing's market salary)
- engagement streak/trends (engagement fit)
- names, avatar, email; cover-letter analysis & coaching narratives (for humans, not the scorer)
- **institution_tier** discarded deliberately; sidesteps the worst India pedigree-bias vector.
---
## 3. THE ENGINE (the v2 redesign)
### 3.0 Core verdict (research-backed)
- **Embedding/cosine similarity must NOT be the ranker** [proven, 3-0]. It rewards topical
word-overlap over qualification fit, can't enforce hard constraints (seniority, certs), and
*collapses on sparse profiles* exactly the v1 failure mode. Embedding = **one input signal**.
- **Winner: a deterministic white-box utility score, fused with other rankers by
WEIGHTED-AVERAGE RANK** [proven: WAvg rank +31.9% nDCG@10; beats RRF +5.8% and every single
ranker]. "Retrieve cheap, rerank expensive" still applies the boards did retrieval; we
spend compute reranking the 25200 fetched listings.
### 3.1 The cascade (per search)
```
① NORMALIZE skills → ESCO canonical; titles → canonical role
② HARD FILTER location/work-mode, must-have certs, seniority floor (deterministic gate —
embeddings CAN'T do this)
③ WHITE-BOX U = Σ_{f∈present} c_f·w_f·φ_f / Σ_{f∈present} c_f·w_f
UTILITY factors: skill_match, experience, location, salary, semantic_sim (ONE input),
industry/company, + QScore-quotient / assessment boosters
④ FUSION weighted-average RANK over: utility ‖ embedding-sim ‖ LLM soft-score
⑤ LLM RERANK GPT-5.4 on top ~20: calibrated 01 fit vs a 6-level rubric;
+ EXPLAIN ALSO logged as cold-start supervision; emits "why" + growth nudge
⑥ OUTPUT ranked list + breakdown + explanation
LOG soft-scores + user save/apply → accrue labels → swap ③④ priors for a learned
pairwise/listwise ranker (margin loss / CMMD — NOT pointwise MSE)
```
### 3.2 Skill matching (the core accuracy lever) [proven method]
Exact taxonomy lookup is a dead end (only 41/202 soft skills exact-match ESCO; real-world
span-match F1 caps ~27%). The stack:
1. **Synonym canonicalization** to ESCO (`k8s→Kubernetes`, `ML→Machine Learning`) seed from
ESCO + v1 role synonyms + India tech terms.
2. **Depth-2 knowledge-graph traversal** of `RELATED_TO` edges for implied/transferable skills
(Kubernetes "container orchestration"). Powers STRETCH.
3. **Contrastive embedding alignment** to the taxonomy to beat the ~27% ceiling.
*Validate locally — the F1@5 0.72 result is on Chinese ads.* v1 ships synonym+KG+cosine-to-ESCO;
upgrade to a trained contrastive aligner later.
4. **Required vs preferred** weighting: required near-gating, preferred additive.
- **Taxonomy = ESCO.** EU-origin sanity-check India role/skill coverage early; extend, don't swap.
### 3.3 Coverage-aware combination [our design — no validated formula exists]
Renormalize over **present** signals only, each weighted by confidence `c_f` (coverage × trust
from the audit): `U = Σ_{present} c_f·w_f·φ_f / Σ_{present} c_f·w_f`.
A missing signal redistributes its weight to what we know never penalizes, never flattens.
This makes the v1 "QScore absent everyone 60" bug impossible by construction. **Validate `c_f`
offline** (confirm it doesn't over-reward sparse profiles).
### 3.4 Cold-start weighting (no labels at launch) [proven]
- **Day 1:** expert-prior weights starting point `skill .35 / experience .25 / location .15 /
salary .10 / semantic .10 / company .05`. **Priors to calibrate, not optima.**
- **From day 1, in parallel:** GPT-5.4 as **teacher** — calibrated 01 soft scores on a 6-level
rubric, logged on every match. Synthesizes supervision (study: discriminative specificity
3.5× — 0.058 → 0.208) instead of waiting for outcomes (<0.05% dense).
- **As save/apply accrues:** train a **pairwise/listwise** ranker + contrastive/paraphrase
augmentation; shift fusion weight off the priors.
### 3.5 FIT vs STRETCH [our design — research left open]
- **FIT:** skill-gap ≈ 0; score current-state overlap; mild over-qualification penalty.
- **STRETCH:** target = one level up / adjacent; score by **reachability** = fraction of the
job's required skills the user *has OR is within depth-2 KG hops of*. "Reach but reachable" =
small, bridgeable gap → this is the prototype's growth nudge.
- Maps onto the Scout intent picker: current-state → "more like this"; aspiration → "step up / new".
---
## 4. Evaluation & model roles — **no human in the loop**
- **Runtime is fully autonomous.** No human ever reviews, approves, or scores a user's matches.
- **Eval grading is automated and cross-model** to avoid self-grading inflation:
| Role | Model |
|---|---|
| Match generation, cold-start soft-score teacher, explanations/growth nudges | **GPT-5.4** |
| Independent accuracy grading (LLM-as-judge) | **Opus 4.8** |
- Cross-family grading kills self-preference bias (no model marks its own homework).
- Residual generic LLM-judge bias (position/verbosity) → prompt hygiene: randomize order,
pairwise, strip length cues.
- **Un-gameable ground truth = real save/apply rate once live.** Opus is the pre-launch proxy.
- **Metrics:** nDCG@10, MRR, P@5, calibration MAE on a synthetic + dogfood gold set
(~50100 India-representative pairs; no hired annotators). Online: interleaving + A/B once live.
- Note: Opus-as-judge is a deliberate, fine exception to "always GPT-5.4" (it's Anthropic, not an
OpenAI call). Eval pipeline needs an Anthropic API key alongside OpenAI.
---
## 5. Transport readiness (do the pipes exist?)
Backbone exists: orchestrator `context_resolver` builds `user_context` from resume-builder +
social-branding `/api/state/{clerk_id}`; matchmaking calls qscore-service directly.
**REVISED 2026-06-16 — the "gaps" were assumptions; the data is LIVE.** Verified by sampling the
prod DBs over SSH (see `SIGNAL_AUDIT_V2.md` §3):
- **✅ READY:** all Tier A + QScore quotient vector.
- **✅ LIVE (was "partial"):** verified LinkedIn title/company/industry — social-branding does a
**live LinkedIn fetch**; LinkedIn education; certifications.
- **✅ LIVE (was "❌ GAP"):** assessment scores (user-service), interview rubrics (`growqr_interview`,
210 reviews — 8-D rubric + video presence + confidence-ratio), roleplay rubrics (`growqr_roleplay`,
Gemini video analysis), improvement velocity (`trend_data`/`historical_comparison`). **None are a
build — they're a query.** *Caveat: filter degenerate sessions (no-show/low-talk poison scores).*
- **Transport already exists:** interview & roleplay services **already receive résumé/LinkedIn
context** via the orchestrator `context_resolver`. **matchmaking-v2 reuses that exact pattern** —
no new transport to build. v1 wiring = consume the assembled `user_context` + one extra read of
course watch-behavior (`growqr_course`).
- **Still: QScore is the competence proxy for v1** (it already consumes interview/roleplay); read
raw rubrics later only if they add lift beyond the aggregate.
---
## 6. Auto-apply (the heart) — "one tap at the end"
> **➜ `AUTOAPPLY_STATE.md` supersedes/extends this section** (validated POC, 2026-06-16). Headline
> update: the original "full auto-submit = NO-GO" is **REVISED** — a server-side **AI browser agent
> (browser-use + gpt-4.1 vision)** is now a *validated, board-agnostic "master key"*:
> - **Proven:** a real application was submitted to OpenAI on AshbyHQ (on-screen success confirmed);
> the *generic* adapter then filled Greenhouse forms it had never seen (4/5) and India-native ATS
> (Zoho Recruit, Keka) with no tuning, honestly bouncing login-walled / no-form pages.
> - **Architecture:** one generic engine + a thin **adapter registry** (Ashby adapter carries earned
> quirks; generic adapter handles the long tail by vision). Verification-first: screenshot-as-
> ground-truth + a hard accuracy gate (never trust the agent's self-report).
> - **Tiered reality:** autonomous for **public ATS (Ashby/Greenhouse/Lever) + India-native ATS**;
> login-gated boards (LinkedIn Easy Apply, Naukri dashboard, Workday/Oracle) stay human/Cloud-assisted.
> - **Economics (measured):** gpt-4.1 **vision-off** ~$0.13/app (clean ATS), ~$0.210.27 (India ATS),
> ~36 min/app; **gpt-4o-mini is a false economy** (failed + not cheaper). Latency needs parallelism.
> - **Anti-bot is the make-or-break risk:** Ashby spam-flagged the *fast* LinkedIn-routed submit;
> the fix was direct-to-ATS + human-like pacing + verified upload. reCAPTCHA/hCaptcha remain a wall
> (simple text CAPTCHAs are vision-solvable). New POC code: `poc/browser-apply/` (the old
> `poc/auto-apply/` mock is retired).
By the time the user taps, all the work is done (found, tailored, filled). The tap ends an
application, it doesn't start one. In the no-install web app, "one tap" resolves two ways:
- **🛰️ Hosted submit** — backend POSTs the application to an **ATS API** (Greenhouse / Lever /
Ashby). True one-tap-done, any device, zero install. **Gated by employer creds** → coverage =
the integrations we provision; grows over time.
- **🔗 Prepared handoff** — one tap opens the board's own page (LinkedIn/Naukri) with résumé +
cover letter + answers staged; user taps submit there. Zero install, any device, zero ban risk.
- **Phase 4 full auto-submit:** NO-GO for now (unattended submit ~46% reliable + ToS-exposed;
the survivor pattern is user-device extension autofill + human submit). Opt-in only when revisited.
POC validates the hosted-submit path end-to-end → see `poc/auto-apply/`.
**Hard rule: never fire test applications at real employers' live postings.** POC-0 = mock ATS;
POC-1 = our own ATS sandbox/test job.
### 6.1 LISTING FRESHNESS / LIVENESS — first-class gate (learned from the POC, June 12)
The personalized demo tapped a real LinkedIn posting that was already "no longer accepting
applications." Then we empirically checked ~7 recent AI-Architect postings: **essentially all
were dead** (closed / HTTP 404 / redirect-to-search = LinkedIn's expired-job behavior). Findings:
- **A perfect match for a dead job is worse than no match** — it's actively undelightful.
- **Permalinks rot in days.** Never hand a user a *cached* posting URL — it's probably dead.
- **Tap-time server-side liveness re-check is unreliable:** LinkedIn serves bot-walled / guest
content (our httpx check false-returned "open" for a job we knew was closed), and expired URLs
302→search or 404. You cannot trust a scrape to tell you a posting is open.
Requirements for v2:
1. **Freshness at the DATA layer:** the on-demand Apify fetch returns posting date / `isExpired` /
applicant count — filter & rank by recency, drop/flag stale **before presenting**. The whole
point of on-demand: every shown listing was fetched seconds ago, so it's fresh by construction.
2. **Honest age signals** in the UI ("posted 3d ago", "closes soon") — derive from posting age
(no scraper returns a true deadline field).
3. **Apply handoff targets a live view**, never a stored permalink: the freshly-fetched listing
or the board's **live search** for the role (always current). Recover gracefully, never dead-end.
---
## 7. Proven vs. validate-locally (honesty ledger)
| Locked & proven (adopt method) | Our design — must validate locally |
|---|---|
| Hybrid fusion > embedding-only ranker | Exact fusion weights (60/25/15 was tuned on a tiny set) |
| Weighted-avg rank > RRF | The coverage-renormalization formula + `c_f` values |
| Synonym + KG-expand + contrastive skills > exact match | FIT/STRETCH reachability scoring |
| LLM-as-teacher for cold-start; pairwise > pointwise | India pedigree/fairness mitigations |
| Embedding-only collapses on sparse profiles | ESCO coverage for India roles |
**Headline accuracy numbers do NOT transfer** (single-paper, small-benchmark, domain-mismatched —
gig matching, Chinese job ads, offline corpora). Adopt the **patterns**, re-tune on GrowQR data.
## 8. Open items
**Resolved 2026-06-16:**
- ✅ Apify inbound validated (Naukri → `muhammetakkurtt`; direct-ATS = autonomous backbone; offsite-
yield measured). ✅ Auto-apply POC validated end-to-end (master key, real submission, India ATS).
- ✅ Signal audit refreshed against live evidence (`SIGNAL_AUDIT_V2.md`). ✅ Agent capability pass
(interview/roleplay strong, Pathways cut). ✅ Slot-in/decommission plan (`DECOMMISSION_PLAN.md`).
**Still open:**
- **Build the engine itself** — wire the v2 input set (Backbone + verified LinkedIn + QScore +
course watch-behavior) via the reused `context_resolver`; implement the §3 cascade.
- Latency benchmark of the actors from ap-south-1; **parallelize apply** (36 min/app is too slow serial).
- **Submit-time anti-bot lane** (the make-or-break): Cloud stealth / CAPTCHA / human-final-tap.
- Build the ~50100 pair India gold set; validate Opus-judge agreement. ESCO India coverage check.
- Label volume for the learned ranker (old corpus was 21 users — accrue via the live save/apply loop).
**UI — in progress (2026-06-17, branch `job-ui-final`):**
- ✅ Scout UI built **in-place in `dashboard-ui/scout`** (not a separate `index.html` POC) around the
locked feature set; screen-by-screen decisions + as-shipped divergences tracked in `SCOUT_UI_SPEC.md`
("BUILD PROGRESS" section). Re-verified honest against this doc — all cuts/improvements, no
unbacked claims. Notable: cascading **Country→City** location (feeds Tier-A `preferred_locations` +
ported location-fit); a **no-resume "Any/open" state** that is the UI face of §3.3 (sparse profile
coverage renormalizes, never fabricates); loader + scope made honest (≤120, on-demand framing).
- 🔧 UI still waiting on the engine for: apply-result statuses (proof screenshots + auto/failedmanual
split), per-job resume-tailoring spinner, and the real signal/match-breakdown wiring behind the cards.
## Key sources (engine research, 2026)
- Synapse (arXiv 2604.02539) hybrid fusion > embedding-only; WAvg rank fusion +31.9% nDCG@10.
- JobMatchAI (arXiv 2603.14558) — white-box utility weights; ESCO synonym + depth-2 KG expansion.
- ConFit / ConFit v2 (arXiv 2401.16349) — contrastive + paraphrase augmentation; label sparsity <0.05%.
- Person-Job Fit distillation (arXiv 2601.10321) LLM-as-teacher soft scores; specificity 0.0580.208; CMMD.
- JobSkape (arXiv 2402.03242) ESCO span-match F1 ~2627% ceiling.
- CareerBERT (arXiv 2503.02056) embedding degradation on short/sparse profiles (coverage collapse).
- Springer 12651-025-00409-x only 41/202 soft skills exact-match ESCO.