Files
matchmaking-v2/research/docs/SIGNAL_AUDIT_V2.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

8.0 KiB

Signal Audit v2 — Evidence-Based Input Inventory

Companion to ENGINE_DESIGN.md. Supersedes its §2/§5 signal audit. Written 2026-06-16, grounded in live prod-DB evidence (sampled over SSH) rather than the original conservative assumptions. Key correction: the v1 doc flagged interview/roleplay/assessment as " GAP — build transport." The data is live and queryable today — richer engine is mostly wiring + discipline, not new builds.


0. Governing discipline (unchanged — the v1-collapse guardrails)

More signals make the engine stronger only if we keep these rules. Breaking them is how v1 died.

  1. Trust ⟂ coverage. A signal earns SCORE WEIGHT only if coverage is high enough that absence isn't the norm. Otherwise it is an ADDITIVE BOOSTER (bonus when present, never a penalty when absent). Most new signals are low-coverage → boosters, not weights.
  2. Filter degenerate data. Verified live: the sampled interview review was a no-show (score 30), the roleplay an 8-word low-talk. These would poison the profile. Drop no_show / low_talk sessions (the data already flags exclude_from_trend: true / scoring_kind: no_show).
  3. Coverage-aware renormalization (ENGINE_DESIGN.md §3.3): score over present signals only; a missing signal redistributes its weight, never flattens.

1. The signal inventory (by tier)

Tier A — Backbone (high coverage → filter + weight)

Signal Source (verified) Role Flows in via
years_experience (date-derived) resume-builder parse weight context-resolver
current_role / title resume + verified LinkedIn filter + weight context-resolver
skills (→ ESCO normalized) resume-builder parse core weight (skill match) context-resolver
location + work mode user prefs (Configure must-haves) + user-service hard filter + 30% weight request prefs
target_roles / industries Configure (user-stated want) filter, not score request prefs

Location logic — PORT from the old engine (matchmaking/app/engine/core.py, it did this better than v1's one-liner). Bring forward _score_location_fit + _location_filter_blocks_match: remote job → 100 (0 if seeker is onsite-only); else city+country substring match vs preferred locations (so an add-your-own typed city "just works"); travel-mode adds travel locations; unknown → neutral 60. As a hard gate: block remote for onsite-only seekers, block no-fit jobs (clamp below threshold) unless the seeker set no location prefs ("Anywhere" = open). Weighted 30%.

Tier B — Verified enrichers (medium coverage → real weight when present)

Signal Source (verified) Role Notes
verified title / company / industry social-branding live LinkedIn fetch weight (present) doc said "partial" — it's live
degree / field / institution resume parse / LinkedIn education weight (present)
certifications resume / LinkedIn booster
assessment scores user-service (assessment tables) booster doc called it a "gap"; it exists
QScore quotient vector (10-D) qscore-service competence proxy (weight, coverage-penalized) see §2 — this is the key aggregate
Market context (demand, hiring velocity, posting freshness, comp band) Perplexity API (online) — TESTED 2026-06-16, returns sourced numbers + honest "can't-verify" on layoffs enricher / context for ranking + the UI "why" cache per role+location (latency/cost). Honest by design — Perplexity flags what it can't confirm (e.g. layoff signals). Replaces the fabricated market-intel that was in the UI signal list.

Tier C — Behavioral / competence boosters (LOW coverage → ADDITIVE ONLY)

Signal Source (verified) Role Notes
Course watch behavior (watch-time, completion, interest profile) growqr_course booster — target-role inference + FIT/STRETCH direction NEW class. Highest-trust: revealed preference > stated. "Completing React content → step toward frontend."
improvement velocity / coachability interview/roleplay trend_data, historical_comparison booster growth trajectory
video on-camera presence interview/roleplay video_analysis (Gemini 2.5 Pro) booster <1% relevant; rarely surfaced

Interview 8-D rubric + roleplay EQ are not listed as independent inputs — see §2 (already folded into QScore).

Labels (training targets, NOT scoring inputs)

Signal Source Use
SAVE / DISMISS / APPLY / RSVP feedback old matchmaking (Record Feedback) + our apply engine outcomes accrue labels → train pairwise/listwise ranker (ENGINE_DESIGN.md §3.1)

2. Entanglement: QScore already consumes interview/roleplay

Do not double-count. QScore is consumed by interview & roleplay, and the interview/roleplay competence is digested into the QScore quotient vector. So for the engine:

  • v1 (recommended): use the QScore quotient vector as the competence proxy — it already incorporates the interview/roleplay rubric signals. This matches ENGINE_DESIGN.md §5: "launch on READY rails (Tier A + QScore quotients), add direct rubric access later as a precision upgrade."
  • Later (precision upgrade): read the raw interview/roleplay rubrics directly as boosters — only once we've confirmed they add lift beyond the QScore aggregate (else it's redundant weight).

This means course watch-behavior is the one genuinely new, non-redundant signal to wire in v1.

3. Transport: mostly already solved

The résumé + LinkedIn → service transport already exists — interview & roleplay services already receive résumé/LinkedIn context (they're résumé/LinkedIn-aware), assembled by the orchestrator's context_resolver into user_context. matchmaking-v2 reuses that exact pattern: consume the assembled user_context (resume + verified LinkedIn + QScore + assessment) the same way interview/ roleplay do. So the rich user-side profile is a wiring job against a proven, existing transport, not a new build. Course watch-behavior is the one extra read (from growqr_course).

The user-side profile is then scored against the on-demand fetched listings (LinkedIn/Naukri/ Indeed via Apify) — the job side stays on-demand; the user side is what these signals enrich.

4. Cut / discard

  • Pathways (entire) — CUT. Feature dropped (weak link: buggy skill-gap, impractical roles) AND its questionnaire is not used as a signal. The Configure screen already captures stated prefs.
  • Carry-over discards (ENGINE_DESIGN.md §2): ATS overall_score (resume quality ≠ fit), parser confidence, vanity metrics, self-declared salary, institution_tier (pedigree bias).
  • UI overclaims with no backing signal: "live status" (tap-time liveness unreliable, §6.1), "in-network" (no referral agent), "Closes in N days" (no deadline data).

5. Net: stronger engine, same discipline

v1 input set = Backbone (resume skills/experience + Configure prefs/filters) + verified LinkedIn + QScore quotient vector (competence proxy) + course watch-behavior (new, high-trust booster), fused coverage-aware, scored against on-demand listings, with save/apply feedback accruing as labels. That is materially richer than the doc's "Tier-A + QScore proxy" — and it's reachable now because the transport exists and the discipline (boosters for sparse, filter degenerate) keeps it from collapsing.

Honesty ledger

Now proven available (was assumed a gap) Still must validate
Interview/roleplay competence (live in DB; via QScore) Does raw rubric add lift beyond QScore?
Course watch-behavior as a signal Coverage of active learners; interest→role mapping quality
Verified LinkedIn identity (live fetch) India LinkedIn fetch reliability
Feedback labels exist (save/apply) Volume sufficient to train a ranker (was 21 users)
Transport reuse (context_resolver) matchmaking-v2 wiring into the mesh