Skip to content

JD match: the noun pass reports a term as missing when the résumé says it in other words #847

Description

@s-annam

Problem

The Missing list is the part of JD match a user acts on — it is the edit queue. So a term that lands there wrongly does not just cost accuracy, it sends someone to make a résumé change that accomplishes nothing.

The noun pass produces exactly that failure. In computeCoverageFromCorpus (src/lib/jd-match/coverage.ts) the two term sources are matched by different rules:

const hit =
  term.source === "skill"
    ? corpusMentionsSkill(corpus, term.id)
    : corpusMentionsPhrase(corpus, term.display);

corpusMentionsSkill resolves through the alias dictionary in skills.ts, so a JD asking for js matches a résumé that says JavaScript. corpusMentionsPhrase matches the JD's surface phrase literally against the corpus. A noun-pass term the résumé expresses in different words is therefore reported as missing, and JdMatch.tsx renders it under Missing (N) indistinguishably from a term the résumé genuinely never addresses.

Concretely, a JD phrase like on-call rotation against a résumé saying production support rotation, or distributed systems against large-scale backend services, is currently a miss.

We have already decided this is a defect once

#607 fixed the same class in the skill-guidance lane — guidance was suggesting skills the résumé already stated in other words. The reasoning there applies unchanged here; only the lane is different. This issue is that decision carried into the noun pass.

Why not just wait for the semantic path

#156's WebLLM requirement matching is the better long-term answer and this issue does not compete with it. But the keyword path is not going away: JdMatchResult types it as a first-class path, and types.ts records it as "also the semantic path's fallback." It is what runs when the model is unavailable, still loading, or declined. A fallback that reports confident false misses is worse than one that is merely coarse, because the user cannot tell which mode produced the list.

Proposal

Narrow the gap in the deterministic path, without pretending it becomes semantic matching.

Two options, and I lean toward the first:

(a) Match noun phrases on a normalized head, not the literal string. Stem/lemmatize and drop determiners and modifiers before comparing, so distributed system and distributed systems stop being different terms, and a JD phrase whose head noun appears in the corpus under a light variation is credited. Cheap, deterministic, testable, no dictionary to maintain.

(b) Extend the alias dictionary to cover common non-skill phrases. More precise where it applies, but it is an open-ended maintenance surface, and skills.ts is deliberately a skill dictionary — its DICTIONARY_VERSION contract and the aliasToId canonicalization exist for chips and guidance, and widening it into general JD prose would change what that version number means.

Either way, a third behaviour is worth having independently: when a term is neither clearly covered nor clearly absent, say so. A "possibly covered — your résumé says X" bucket is more honest than forcing a binary, and it is directly actionable in a way a false miss is not. CoverageResult is { covered, missing } today, so this is a shape change and needs to be weighed against every current consumer, including the score.

Scoring

Any change here moves score. That is correct — the current score is inflated in the other direction by counting real coverage as missing — but it must be a deliberate, recorded move rather than a silent drift, since the number appears in the UI and users compare it across runs. If a middle bucket lands, decide its weight explicitly rather than defaulting it to zero or to NOUN_WEIGHT.

Out of scope

Acceptance criteria

  • A JD noun phrase the résumé states in different words is not reported under Missing
  • A JD term the résumé genuinely does not address is still reported under Missing
  • Where the match is uncertain, the UI distinguishes it from a confident miss rather than picking a side silently
  • The effect on score is deliberate, documented in the module docblock, and covered by a test that pins the new weighting
  • Fixtures cover the plural/inflection case and the different-wording case explicitly
  • There remains exactly one coverage implementation — computeCoverageFromCorpus stays the only matcher, per its own docblock

Metadata

Metadata

Assignees

No one assigned

    Labels

    improvementEnhancing existing functionalityux:score-clarityUX program: score explainability and what-to-fix guidance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions