You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On /jobs/'s paste-a-JD panel (PasteJdPanel.tsx), once a user opts into on-device semantic JD-match analysis and a semantic verdict list is on screen, the "Tailor résumé to this job" button's visibility and payload are still built exclusively from the keyword-coverage path. A user can be looking directly at a semantic "Missing" verdict for a real requirement gap and have no button to act on it — or have the button present but silently uninformed about the gap they can see.
This is a known, deliberately-scoped cut from #204/PR #866 (the semantic verdict UI), not an oversight in that PR — its own added comment says so explicitly. This issue is the tracked follow-up to close the gap, filed from the PR #866 review (gh-204-jdmatch-semantic-ui).
Concrete failure scenario
User pastes a JD whose text literally contains a skill word (e.g. "Kubernetes") that also appears verbatim somewhere in their résumé — so keyword coverage counts it as covered, and coverage.missing is empty.
User ticks "Analyze with on-device AI". The semantic judge evaluates the actual requirement behind that word (e.g. "3+ years operating production Kubernetes clusters") against the résumé's real content, not just the word's presence, and correctly returns status: "missing" with a grounded one-sentence reason.
The SemanticMatch verdict list on screen shows that requirement under "Missing".
buildJdRewriteContext(jdMatch.coverage) (keyword-only) sees coverage.missing.length === 0 and returns null, so the "Tailor résumé to this job" button does not render at all (onTailor && jdContext !== null gate) — the user has no way to steer a rewrite at the exact gap they're looking at.
The inverse also happens: a term is genuinely missing by keyword match but the semantic judge marks it partial (résumé shows adjacent experience) — the button renders with keyword-only steering text, silently ignoring the more nuanced semantic read the user opted in to get.
Root cause
src/components/features/PasteJdPanel.tsx:99 — displayed = semanticResult ?? jdMatch is what's rendered on screen (semantic verdicts replace the keyword columns once a semantic run finishes).
src/components/features/PasteJdPanel.tsx:111-113 — but jdContext (the button's gate AND payload) is computed as jdMatch === null ? null : buildJdRewriteContext(jdMatch.coverage) — always the keyword arm, regardless of what displayed currently is.
src/lib/jd-match/rewrite-context.ts:34-45 — buildJdRewriteContext(coverage: CoverageResult): string | null only knows how to read a keyword CoverageResult.missing: ExtractedTerm[]. There is no equivalent function that reads RequirementVerdict[] (the semantic result's verdicts field, src/lib/jd-match/llm/judge-evidence.ts:53-59: { requirement, status, reason, evidence? }).
The semantic result's shape (src/lib/jd-match/types.ts:47-48: { verdicts: readonly RequirementVerdict[], summary: SemanticMatchSummary }) is a superset of what the keyword path can express — it carries partial as a distinct status and a grounded reason/evidence, neither of which buildJdRewriteContext has any way to use.
Implementation plan
Add a semantic-aware steering builder in src/lib/jd-match/rewrite-context.ts, sibling to buildJdRewriteContext:
Reuse MAX_TERMS and the existing no-fabrication phrasing pattern rather than inventing new copy — read buildJdRewriteContext's existing return statement first and match its voice.
Wire it into PasteJdPanel.tsx. Replace the keyword-only jdContextuseMemo (lines 111-113) with one that reads from whichever result is currently displayed:
Update the comment block above it (currently states "Built from the KEYWORD coverage regardless of which view is on screen") to describe the new behavior instead of leaving stale reasoning in place.
Same treatment for JobResultCard, if it has an equivalent tailor-button/jdContext wiring — check whether it can ever reach a semantic result (per PR feat(jd-match): semantic verdict UI, on-device opt-in, keyword fallback view (#204) #866's KeywordMatch/SemanticMatch docblocks, RankedJob.jdMatch is typed KeywordJdMatch only, so JobResultCard may be keyword-only by construction and this step may be a no-op; confirm before skipping).
Semantic result with missing/partial verdicts, keyword coverage fully covered → button renders, onTailor receives semantic-derived steering text (not null).
Semantic result with all verdicts met → button does not render (parity with today's coverage.missing.length === 0 behavior, just sourced from verdicts).
Keyword-only path (opt-in off, or opted-in but degraded to keyword) → unchanged behavior, still keyword-derived (regression guard).
Unit test for buildJdRewriteContextFromVerdicts in a new/extended rewrite-context.test.ts: empty verdicts → null; MAX_TERMS cap respected; partial and missing both included, met excluded.
Acceptance criteria
buildJdRewriteContextFromVerdicts exists in src/lib/jd-match/rewrite-context.ts, exported, unit-tested (empty → null, cap respected, only partial/missing included).
PasteJdPanel.tsx's jdContext is derived from semanticResult.verdicts when a semantic result is displayed, and falls back to keyword coverage otherwise — verified by a test where keyword coverage is fully covered but a semantic verdict is missing, and the button renders with non-null steering text.
Regression: with semantic opt-in off (or opted-in but degraded to keyword), jdContext behavior is byte-identical to pre-fix (existing PasteJdPanel.semantic.test.tsx / PasteJdPanel keyword tests still pass unmodified).
npm run typecheck, npm run lint, and the full PasteJdPanel/rewrite-context test files pass.
The stale "Built from the KEYWORD coverage regardless of which view is on screen" comment in PasteJdPanel.tsx is updated to match the new behavior (no misleading docblock left behind).
Reuse analysis
Capability: steer a résumé rewrite using JD-gap information. Existing surfaces found:buildJdRewriteContext (src/lib/jd-match/rewrite-context.ts) already owns this for the keyword path; PasteJdPanel.tsx's jdContext/"Tailor résumé to this job" Button already owns the UI trigger and is reused by JobResultCard per its own docblock. Decision: extend rewrite-context.ts with a sibling function for the semantic shape and extend PasteJdPanel.tsx's existing jdContext derivation — no new component, panel, or button. This is a data-source fix to an existing surface, not a new workflow surface, so no Reuse Gate concern beyond "don't hand-roll a second button," which this plan doesn't do.
Context
Surfaced during the /pr-review of #866 (feat(jd-match): semantic verdict UI, on-device opt-in, keyword fallback view (#204)), review comment: #866 (review) (Secondary finding #2). Not blocking for #866 — the cut was already disclosed in that PR's own code comments — but tracked here so it isn't lost.
Problem
On
/jobs/'s paste-a-JD panel (PasteJdPanel.tsx), once a user opts into on-device semantic JD-match analysis and a semantic verdict list is on screen, the "Tailor résumé to this job" button's visibility and payload are still built exclusively from the keyword-coverage path. A user can be looking directly at a semantic "Missing" verdict for a real requirement gap and have no button to act on it — or have the button present but silently uninformed about the gap they can see.This is a known, deliberately-scoped cut from #204/PR #866 (the semantic verdict UI), not an oversight in that PR — its own added comment says so explicitly. This issue is the tracked follow-up to close the gap, filed from the PR #866 review (
gh-204-jdmatch-semantic-ui).Concrete failure scenario
coverage.missingis empty.status: "missing"with a grounded one-sentence reason.SemanticMatchverdict list on screen shows that requirement under "Missing".buildJdRewriteContext(jdMatch.coverage)(keyword-only) seescoverage.missing.length === 0and returnsnull, so the "Tailor résumé to this job" button does not render at all (onTailor && jdContext !== nullgate) — the user has no way to steer a rewrite at the exact gap they're looking at.The inverse also happens: a term is genuinely missing by keyword match but the semantic judge marks it
partial(résumé shows adjacent experience) — the button renders with keyword-only steering text, silently ignoring the more nuanced semantic read the user opted in to get.Root cause
src/components/features/PasteJdPanel.tsx:99—displayed = semanticResult ?? jdMatchis what's rendered on screen (semantic verdicts replace the keyword columns once a semantic run finishes).src/components/features/PasteJdPanel.tsx:111-113— butjdContext(the button's gate AND payload) is computed asjdMatch === null ? null : buildJdRewriteContext(jdMatch.coverage)— always the keyword arm, regardless of whatdisplayedcurrently is.src/lib/jd-match/rewrite-context.ts:34-45—buildJdRewriteContext(coverage: CoverageResult): string | nullonly knows how to read a keywordCoverageResult.missing: ExtractedTerm[]. There is no equivalent function that readsRequirementVerdict[](the semantic result'sverdictsfield,src/lib/jd-match/llm/judge-evidence.ts:53-59:{ requirement, status, reason, evidence? }).src/lib/jd-match/types.ts:47-48:{ verdicts: readonly RequirementVerdict[], summary: SemanticMatchSummary }) is a superset of what the keyword path can express — it carriespartialas a distinct status and a groundedreason/evidence, neither of whichbuildJdRewriteContexthas any way to use.Implementation plan
Add a semantic-aware steering builder in
src/lib/jd-match/rewrite-context.ts, sibling tobuildJdRewriteContext:Reuse
MAX_TERMSand the existing no-fabrication phrasing pattern rather than inventing new copy — readbuildJdRewriteContext's existing return statement first and match its voice.Wire it into
PasteJdPanel.tsx. Replace the keyword-onlyjdContextuseMemo(lines 111-113) with one that reads from whichever result is currentlydisplayed:Update the comment block above it (currently states "Built from the KEYWORD coverage regardless of which view is on screen") to describe the new behavior instead of leaving stale reasoning in place.
Same treatment for
JobResultCard, if it has an equivalent tailor-button/jdContextwiring — check whether it can ever reach a semantic result (per PR feat(jd-match): semantic verdict UI, on-device opt-in, keyword fallback view (#204) #866'sKeywordMatch/SemanticMatchdocblocks,RankedJob.jdMatchis typedKeywordJdMatchonly, soJobResultCardmay be keyword-only by construction and this step may be a no-op; confirm before skipping).Tests (
src/components/features/PasteJdPanel.semantic.test.tsxalready has the mock scaffolding for semantic results from feat(jd-match): semantic verdict UI, on-device opt-in, keyword fallback view (#204) #866 — extend it):missing/partialverdicts, keyword coverage fully covered → button renders,onTailorreceives semantic-derived steering text (notnull).met→ button does not render (parity with today'scoverage.missing.length === 0behavior, just sourced from verdicts).buildJdRewriteContextFromVerdictsin a new/extendedrewrite-context.test.ts: empty verdicts →null;MAX_TERMScap respected;partialandmissingboth included,metexcluded.Acceptance criteria
buildJdRewriteContextFromVerdictsexists insrc/lib/jd-match/rewrite-context.ts, exported, unit-tested (empty → null, cap respected, onlypartial/missingincluded).PasteJdPanel.tsx'sjdContextis derived fromsemanticResult.verdictswhen a semantic result isdisplayed, and falls back to keywordcoverageotherwise — verified by a test where keyword coverage is fully covered but a semantic verdict ismissing, and the button renders with non-null steering text.jdContextbehavior is byte-identical to pre-fix (existingPasteJdPanel.semantic.test.tsx/PasteJdPanelkeyword tests still pass unmodified).npm run typecheck,npm run lint, and the fullPasteJdPanel/rewrite-contexttest files pass.PasteJdPanel.tsxis updated to match the new behavior (no misleading docblock left behind).Reuse analysis
Capability: steer a résumé rewrite using JD-gap information. Existing surfaces found:
buildJdRewriteContext(src/lib/jd-match/rewrite-context.ts) already owns this for the keyword path;PasteJdPanel.tsx'sjdContext/"Tailor résumé to this job"Buttonalready owns the UI trigger and is reused byJobResultCardper its own docblock. Decision: extendrewrite-context.tswith a sibling function for the semantic shape and extendPasteJdPanel.tsx's existingjdContextderivation — no new component, panel, or button. This is a data-source fix to an existing surface, not a new workflow surface, so no Reuse Gate concern beyond "don't hand-roll a second button," which this plan doesn't do.Context
Surfaced during the
/pr-reviewof #866 (feat(jd-match): semantic verdict UI, on-device opt-in, keyword fallback view (#204)), review comment: #866 (review) (Secondary finding #2). Not blocking for #866 — the cut was already disclosed in that PR's own code comments — but tracked here so it isn't lost.