Vibecode CVMatchScore
track this build4 phases, 12 steps, beginner friendly0%The core loop, resume plus job posting into an LLM holding a scoring rubric, is one prompt and an afternoon, and for improving one resume against one posting it genuinely works. The honest gap is calibration: a rubric you wrote today measures today's mood, two runs of the same resume can disagree, and a 72 means nothing without a baseline of scored applications behind it. Fine as a mirror, thin as a measuring stick.
You are building a lean indie version of CVMatchScore. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # CVMatchScore · indie build A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Parsing | Structured extraction from both documents with positions kept | every claim the tool makes must be traceable to text in one of them | | Requirements | Extracted from the posting as discrete, quoted items | a requirement is a sentence, not a keyword | | Evidence | Matched to spans in the CV, or explicitly absent | absence is the useful output and must be stated as such | | Refusal | No aggregate score anywhere in the model or interface | a number that cannot be checked will be believed anyway | | Runtime | TypeScript with a local application | a CV and a salary history are personal and need not leave the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A layout-aware document extractor** · free - Why: Designed CV templates are two-column and a naive extractor produces interleaved text that every later step then operates on. - Get it: A library exposing text with positions and block structure. - Verify: A two-column CV extracts in correct reading order - [ ] **A local model for extraction** · free, needs memory - Why: A CV and the jobs someone is applying for are private in ways people underestimate, and this runs over both. - Get it: Any local runner with a model that returns structured output reliably. - Verify: Ten extractions return valid structured output with positions - [ ] **Your own CV and ten real postings** · free - Why: Requirement extraction quality is only assessable against real postings, which vary from precise lists to three paragraphs of aspiration. - Get it: Collect postings you would actually apply for, including a vague one and one heavy with boilerplate. - Verify: Ten postings exist including one vague and one boilerplate-heavy - [ ] **What you will tell users about their chances** · free - Why: The whole build refuses to predict, and the wording of that refusal is the most important text in the interface. - Get it: Write the sentence the report leads with about what the comparison does and does not tell them. - Verify: A written sentence exists and appears where the reader will see it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. - a calibrated rubric that scores the same resume the same way twice - 50+ language support tested per parameter - DOC, DOCX, and RTF parsing beyond PDF - improvement plans and tailored cover letters built from the same analysis - scores comparable across weeks of applications If one of those is essential to you, that is the reason to keep paying for CVMatchScore, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · CVMatchScore The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a comparison between your CV and a job description, without inventing a score to replace CVMatchScore. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The premise this category sells is false, and you should know that before building it. The claim that around seventy five percent of applications are rejected by an applicant tracking system before a human sees them has no credible source. It traces back to a resume software vendor pitching it around 2012, which went out of business in 2013 without ever publishing a method. When recruiters are asked directly, the overwhelming majority say their systems do not automatically reject anyone: an applicant tracking system files and parses applications into searchable fields, and when parsing fails it routes the record to a human to fix rather than to the bin. So a tool that outputs a number out of a hundred, representing your chances against a gatekeeper that does not work that way, is selling anxiety. It is also unfalsifiable, since nobody can check the score against anything. What is genuinely useful is narrower and honest: show which requirements in the posting your CV does not evidence, quote where it does, and say plainly that the real reason applications go unanswered is usually that several hundred arrived in two days. That is what this builds. ### Stack (fixed, do not substitute) - Parsing: Structured extraction from both documents with positions kept · every claim the tool makes must be traceable to text in one of them - Requirements: Extracted from the posting as discrete, quoted items · a requirement is a sentence, not a keyword - Evidence: Matched to spans in the CV, or explicitly absent · absence is the useful output and must be stated as such - Refusal: No aggregate score anywhere in the model or interface · a number that cannot be checked will be believed anyway - Runtime: TypeScript with a local application · a CV and a salary history are personal and need not leave the machine ### Data model (create this before Phase 1) - documents(id, kind, text, filename, added_at) · kind is cv or posting - requirements(id, posting_id, quote, char_start, char_end, category, is_essential) · essential where the posting says so, never inferred - evidence(requirement_id, cv_id, quote, char_start, char_end, strength, note) - gaps(requirement_id, cv_id, kind, note) · kind is absent, weak or unclear - rewrites(id, cv_id, requirement_id, suggestion, accepted_at) · suggestions, never applied automatically - sessions(id, cv_id, posting_id, compared_at) - There is deliberately no score column anywhere, because a stored number would be displayed and it would be invented. ### Phase 1 · Reading both documents properly Build: Extract text from common CV formats with layout awareness, and from a posting pasted or fetched. Keep character positions so every later claim can quote. Report a document that could not be parsed rather than producing partial text. Trap: A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. Done when: A two-column CV extracts in correct reading order, verified by reading the output. Character positions are preserved so a quote can be located. A document that fails to parse is reported rather than producing partial text. A posting pasted as plain text works identically to a fetched one. Do not build yet: No requirements, matching or output. ### Phase 2 · Requirements as sentences, not keywords Build: Extract requirements from the posting as quoted items with their positions. Distinguish stated essentials from desirables using the posting's own language rather than inferring. Ignore boilerplate that is not a requirement. Trap: Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. Done when: Requirements are extracted as quoted sentences with positions rather than as keywords. Essentials are distinguished only where the posting says so, never inferred, verified against a posting that makes no such distinction. Boilerplate about company culture is not extracted as a requirement. A posting with vague requirements produces few rather than invented ones. Do not build yet: No matching or output yet. ### Phase 3 · Evidence, or its absence, stated plainly Build: Match each requirement to spans in the CV, quoting both sides. Where there is no evidence, say so explicitly rather than scoring it low. Distinguish absent from present but weakly evidenced. Trap: Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. Done when: Every requirement resolves to quoted CV evidence or an explicit absence. Weak evidence is distinguished from absence with both quoted. No percentage or partial-match number appears anywhere, verified by inspecting the output. A requirement the CV addresses in different words is matched and the difference is visible. Do not build yet: No suggestions or reporting yet. ### Phase 4 · Suggestions that do not write your CV Build: Suggest what a gap would need evidence of, phrased as a question about your experience rather than as text to insert. Never generate CV sentences. Let a suggestion be dismissed as not applicable. Trap: Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. Done when: Suggestions are phrased as questions about your experience rather than as insertable text. No generated CV sentence appears anywhere, verified by inspecting the output. A suggestion can be dismissed as not applicable and stays dismissed. Accepting a suggestion opens your own editor rather than inserting text. Do not build yet: No reporting or history yet. ### Phase 5 · Saying what the tool cannot tell you Build: Produce a report listing evidenced requirements, weak ones and absences with quotes, stating plainly what it does not know. Keep everything local. Track applications so you can see your own patterns rather than a predicted outcome. Trap: A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. Done when: The report states what the comparison does not tell you, prominently rather than in a footer. Evidenced, weak and absent requirements are listed with quotes from both documents. Everything runs locally, verified with networking disabled. Application tracking records what you sent and what happened without predicting anything. Do not build yet: Nothing after this. ### Out of scope (and why) - Any overall score or match percentage, which is the feature the category sells and which measures nothing real. - Predicting whether you will be contacted, which depends mostly on how many others applied. - Generating CV content, which invites claims about experience you do not have. - Optimising for a specific applicant tracking system, which is optimising against a system that does not reject you. ### README must contain - That the claim about most applications being auto-rejected has no credible source and traces to a defunct vendor - That applicant tracking systems file and parse applications rather than rejecting them - That no overall score exists anywhere in the model or the interface - That requirements are quoted sentences rather than keywords - That suggestions are questions about your experience rather than text to paste in ===== AGENTS.md ===== # Agent instructions · CVMatchScore indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Structured extraction from both documents with positions kept, Extracted from the posting as discrete, quoted items, Matched to spans in the CV, or explicitly absent, No aggregate score anywhere in the model or interface, TypeScript with a local application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ===== BUILD_PLAN.md ===== # Build plan · CVMatchScore A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading both documents properly A CV and a posting parse into text with positions preserved. ### Steps 1. Extract with layout awareness Designed CV templates are two-column and scramble under naive extraction. Files: `src/extract.ts` 2. Preserve character positions Every claim later has to quote, which needs positions. Files: `src/documents.ts` 3. Report parse failures Partial text produces a comparison that is confidently wrong. Files: `src/extract.ts` ### Done when - [ ] A two-column CV extracts in reading order - [ ] Character positions are preserved - [ ] Parse failures are reported - [ ] Pasted and fetched postings behave identically ### Watch out - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. ## Phase 2 · Requirements as sentences, not keywords The posting yields discrete requirements you could argue with. ### Steps 1. Extract quoted sentences A keyword loses what was actually being asked for. Files: `src/requirements.ts` 2. Use the posting's own essential language Inferring which are essential is guessing at someone else's priorities. Files: `src/requirements.ts` 3. Skip boilerplate Culture paragraphs are not requirements and dilute the comparison. Files: `src/requirements.ts` ### Done when - [ ] Requirements are quoted sentences with positions - [ ] Essentials come from the posting's own language - [ ] Boilerplate is not extracted - [ ] A vague posting yields few requirements ### Watch out - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. ## Phase 3 · Evidence, or its absence, stated plainly Each requirement is matched to CV text or reported as unevidenced. ### Steps 1. Quote both sides of every match So the judgement belongs to the person rather than the tool. Files: `src/match.ts` 2. Distinguish absent from weak They call for completely different responses. Files: `src/gaps.ts` 3. Emit no partial-match numbers A percentage hides exactly the distinction that matters. Files: `tests/no-scores.test.ts` ### Done when - [ ] Every requirement resolves to quoted evidence or explicit absence - [ ] Weak and absent are distinguished with quotes - [ ] No percentages appear anywhere - [ ] Differently worded matches are found and visible ### Watch out - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. ## Phase 4 · Suggestions that do not write your CV You are told what is missing, not given sentences to paste in. ### Steps 1. Phrase gaps as questions What did you actually do, rather than here is what they wanted. Files: `src/suggest.ts` 2. Generate no CV text Its absence is the design. A pasted claim is a lie at interview. Files: `tests/no-generation.test.ts` 3. Keep dismissals Some requirements genuinely do not apply to you. Files: `src/suggest.ts` ### Done when - [ ] Suggestions are questions, not insertable text - [ ] No CV sentences are generated - [ ] Dismissals persist - [ ] Accepting opens your own editor ### Watch out - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ## Not in this build - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. ## After v1, if you want it - Comparing one CV against several postings to see which requirements recur - Tracking which of your own claims get asked about at interview, which is real feedback - A version history of your CV, since most people keep six files and lose track ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your CV and the jobs you are applying for. Personal in both directions. DB_PATH=~/.local/share/cvmatch/data.db # Required. Runs over your CV and your job search. Local is the sensible default. EXTRACTION_MODEL=local # Required. Present as a constant so the answer is visible. There is nothing real to score. EMIT_SCORE=never # Required. Present as a constant. A pasted claim becomes a lie at interview. GENERATE_CV_TEXT=never # Required. Present as a constant. Neither document needs to leave the machine. ALLOW_NETWORK=never
You are building a lean indie version of CVMatchScore. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # CVMatchScore · indie build A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Parsing | Structured extraction from both documents with positions kept | every claim the tool makes must be traceable to text in one of them | | Requirements | Extracted from the posting as discrete, quoted items | a requirement is a sentence, not a keyword | | Evidence | Matched to spans in the CV, or explicitly absent | absence is the useful output and must be stated as such | | Refusal | No aggregate score anywhere in the model or interface | a number that cannot be checked will be believed anyway | | Runtime | TypeScript with a local application | a CV and a salary history are personal and need not leave the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A layout-aware document extractor** · free - Why: Designed CV templates are two-column and a naive extractor produces interleaved text that every later step then operates on. - Get it: A library exposing text with positions and block structure. - Verify: A two-column CV extracts in correct reading order - [ ] **A local model for extraction** · free, needs memory - Why: A CV and the jobs someone is applying for are private in ways people underestimate, and this runs over both. - Get it: Any local runner with a model that returns structured output reliably. - Verify: Ten extractions return valid structured output with positions - [ ] **Your own CV and ten real postings** · free - Why: Requirement extraction quality is only assessable against real postings, which vary from precise lists to three paragraphs of aspiration. - Get it: Collect postings you would actually apply for, including a vague one and one heavy with boilerplate. - Verify: Ten postings exist including one vague and one boilerplate-heavy - [ ] **What you will tell users about their chances** · free - Why: The whole build refuses to predict, and the wording of that refusal is the most important text in the interface. - Get it: Write the sentence the report leads with about what the comparison does and does not tell them. - Verify: A written sentence exists and appears where the reader will see it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. - a calibrated rubric that scores the same resume the same way twice - 50+ language support tested per parameter - DOC, DOCX, and RTF parsing beyond PDF - improvement plans and tailored cover letters built from the same analysis - scores comparable across weeks of applications If one of those is essential to you, that is the reason to keep paying for CVMatchScore, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · CVMatchScore The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a comparison between your CV and a job description, without inventing a score to replace CVMatchScore. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The premise this category sells is false, and you should know that before building it. The claim that around seventy five percent of applications are rejected by an applicant tracking system before a human sees them has no credible source. It traces back to a resume software vendor pitching it around 2012, which went out of business in 2013 without ever publishing a method. When recruiters are asked directly, the overwhelming majority say their systems do not automatically reject anyone: an applicant tracking system files and parses applications into searchable fields, and when parsing fails it routes the record to a human to fix rather than to the bin. So a tool that outputs a number out of a hundred, representing your chances against a gatekeeper that does not work that way, is selling anxiety. It is also unfalsifiable, since nobody can check the score against anything. What is genuinely useful is narrower and honest: show which requirements in the posting your CV does not evidence, quote where it does, and say plainly that the real reason applications go unanswered is usually that several hundred arrived in two days. That is what this builds. ### Stack (fixed, do not substitute) - Parsing: Structured extraction from both documents with positions kept · every claim the tool makes must be traceable to text in one of them - Requirements: Extracted from the posting as discrete, quoted items · a requirement is a sentence, not a keyword - Evidence: Matched to spans in the CV, or explicitly absent · absence is the useful output and must be stated as such - Refusal: No aggregate score anywhere in the model or interface · a number that cannot be checked will be believed anyway - Runtime: TypeScript with a local application · a CV and a salary history are personal and need not leave the machine ### Data model (create this before Phase 1) - documents(id, kind, text, filename, added_at) · kind is cv or posting - requirements(id, posting_id, quote, char_start, char_end, category, is_essential) · essential where the posting says so, never inferred - evidence(requirement_id, cv_id, quote, char_start, char_end, strength, note) - gaps(requirement_id, cv_id, kind, note) · kind is absent, weak or unclear - rewrites(id, cv_id, requirement_id, suggestion, accepted_at) · suggestions, never applied automatically - sessions(id, cv_id, posting_id, compared_at) - There is deliberately no score column anywhere, because a stored number would be displayed and it would be invented. ### Phase 1 · Reading both documents properly Build: Extract text from common CV formats with layout awareness, and from a posting pasted or fetched. Keep character positions so every later claim can quote. Report a document that could not be parsed rather than producing partial text. Trap: A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. Done when: A two-column CV extracts in correct reading order, verified by reading the output. Character positions are preserved so a quote can be located. A document that fails to parse is reported rather than producing partial text. A posting pasted as plain text works identically to a fetched one. Do not build yet: No requirements, matching or output. ### Phase 2 · Requirements as sentences, not keywords Build: Extract requirements from the posting as quoted items with their positions. Distinguish stated essentials from desirables using the posting's own language rather than inferring. Ignore boilerplate that is not a requirement. Trap: Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. Done when: Requirements are extracted as quoted sentences with positions rather than as keywords. Essentials are distinguished only where the posting says so, never inferred, verified against a posting that makes no such distinction. Boilerplate about company culture is not extracted as a requirement. A posting with vague requirements produces few rather than invented ones. Do not build yet: No matching or output yet. ### Phase 3 · Evidence, or its absence, stated plainly Build: Match each requirement to spans in the CV, quoting both sides. Where there is no evidence, say so explicitly rather than scoring it low. Distinguish absent from present but weakly evidenced. Trap: Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. Done when: Every requirement resolves to quoted CV evidence or an explicit absence. Weak evidence is distinguished from absence with both quoted. No percentage or partial-match number appears anywhere, verified by inspecting the output. A requirement the CV addresses in different words is matched and the difference is visible. Do not build yet: No suggestions or reporting yet. ### Phase 4 · Suggestions that do not write your CV Build: Suggest what a gap would need evidence of, phrased as a question about your experience rather than as text to insert. Never generate CV sentences. Let a suggestion be dismissed as not applicable. Trap: Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. Done when: Suggestions are phrased as questions about your experience rather than as insertable text. No generated CV sentence appears anywhere, verified by inspecting the output. A suggestion can be dismissed as not applicable and stays dismissed. Accepting a suggestion opens your own editor rather than inserting text. Do not build yet: No reporting or history yet. ### Phase 5 · Saying what the tool cannot tell you Build: Produce a report listing evidenced requirements, weak ones and absences with quotes, stating plainly what it does not know. Keep everything local. Track applications so you can see your own patterns rather than a predicted outcome. Trap: A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. Done when: The report states what the comparison does not tell you, prominently rather than in a footer. Evidenced, weak and absent requirements are listed with quotes from both documents. Everything runs locally, verified with networking disabled. Application tracking records what you sent and what happened without predicting anything. Do not build yet: Nothing after this. ### Out of scope (and why) - Any overall score or match percentage, which is the feature the category sells and which measures nothing real. - Predicting whether you will be contacted, which depends mostly on how many others applied. - Generating CV content, which invites claims about experience you do not have. - Optimising for a specific applicant tracking system, which is optimising against a system that does not reject you. ### README must contain - That the claim about most applications being auto-rejected has no credible source and traces to a defunct vendor - That applicant tracking systems file and parse applications rather than rejecting them - That no overall score exists anywhere in the model or the interface - That requirements are quoted sentences rather than keywords - That suggestions are questions about your experience rather than text to paste in ===== AGENTS.md ===== # Agent instructions · CVMatchScore indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Structured extraction from both documents with positions kept, Extracted from the posting as discrete, quoted items, Matched to spans in the CV, or explicitly absent, No aggregate score anywhere in the model or interface, TypeScript with a local application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ===== BUILD_PLAN.md ===== # Build plan · CVMatchScore A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading both documents properly A CV and a posting parse into text with positions preserved. ### Steps 1. Extract with layout awareness Designed CV templates are two-column and scramble under naive extraction. Files: `src/extract.ts` 2. Preserve character positions Every claim later has to quote, which needs positions. Files: `src/documents.ts` 3. Report parse failures Partial text produces a comparison that is confidently wrong. Files: `src/extract.ts` ### Done when - [ ] A two-column CV extracts in reading order - [ ] Character positions are preserved - [ ] Parse failures are reported - [ ] Pasted and fetched postings behave identically ### Watch out - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. ## Phase 2 · Requirements as sentences, not keywords The posting yields discrete requirements you could argue with. ### Steps 1. Extract quoted sentences A keyword loses what was actually being asked for. Files: `src/requirements.ts` 2. Use the posting's own essential language Inferring which are essential is guessing at someone else's priorities. Files: `src/requirements.ts` 3. Skip boilerplate Culture paragraphs are not requirements and dilute the comparison. Files: `src/requirements.ts` ### Done when - [ ] Requirements are quoted sentences with positions - [ ] Essentials come from the posting's own language - [ ] Boilerplate is not extracted - [ ] A vague posting yields few requirements ### Watch out - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. ## Phase 3 · Evidence, or its absence, stated plainly Each requirement is matched to CV text or reported as unevidenced. ### Steps 1. Quote both sides of every match So the judgement belongs to the person rather than the tool. Files: `src/match.ts` 2. Distinguish absent from weak They call for completely different responses. Files: `src/gaps.ts` 3. Emit no partial-match numbers A percentage hides exactly the distinction that matters. Files: `tests/no-scores.test.ts` ### Done when - [ ] Every requirement resolves to quoted evidence or explicit absence - [ ] Weak and absent are distinguished with quotes - [ ] No percentages appear anywhere - [ ] Differently worded matches are found and visible ### Watch out - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. ## Phase 4 · Suggestions that do not write your CV You are told what is missing, not given sentences to paste in. ### Steps 1. Phrase gaps as questions What did you actually do, rather than here is what they wanted. Files: `src/suggest.ts` 2. Generate no CV text Its absence is the design. A pasted claim is a lie at interview. Files: `tests/no-generation.test.ts` 3. Keep dismissals Some requirements genuinely do not apply to you. Files: `src/suggest.ts` ### Done when - [ ] Suggestions are questions, not insertable text - [ ] No CV sentences are generated - [ ] Dismissals persist - [ ] Accepting opens your own editor ### Watch out - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ## Not in this build - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. ## After v1, if you want it - Comparing one CV against several postings to see which requirements recur - Tracking which of your own claims get asked about at interview, which is real feedback - A version history of your CV, since most people keep six files and lose track ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your CV and the jobs you are applying for. Personal in both directions. DB_PATH=~/.local/share/cvmatch/data.db # Required. Runs over your CV and your job search. Local is the sensible default. EXTRACTION_MODEL=local # Required. Present as a constant so the answer is visible. There is nothing real to score. EMIT_SCORE=never # Required. Present as a constant. A pasted claim becomes a lie at interview. GENERATE_CV_TEXT=never # Required. Present as a constant. Neither document needs to leave the machine. ALLOW_NETWORK=never
You are building a production product version of CVMatchScore. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # CVMatchScore · product brief ## Problem The core loop, resume plus job posting into an LLM holding a scoring rubric, is one prompt and an afternoon, and for improving one resume against one posting it genuinely works. The honest gap is calibration: a rubric you wrote today measures today's mood, two runs of the same resume can disagree, and a 72 means nothing without a baseline of scored applications behind it. Fine as a mirror, thin as a measuring stick. ## Product outcome A comparison that shows exactly which stated requirements your CV does not evidence, quotes both sides, and refuses to invent a number about your chances. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - OpenAI or Anthropic API key - pdf-parse or pdfplumber for resume text extraction - a written rubric with a 0-10 definition per criterion - a few real resume and posting pairs to sanity-check the scores ## Explicit non-goals for v1 - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. - a calibrated rubric that scores the same resume the same way twice - 50+ language support tested per parameter - DOC, DOCX, and RTF parsing beyond PDF - improvement plans and tailored cover letters built from the same analysis - scores comparable across weeks of applications ## Success criteria - A two-column CV is confirmed to extract in reading order - Requirements are confirmed to be quoted sentences - No score or percentage is confirmed present anywhere - No CV text is confirmed generated - Everything is confirmed to work with networking disabled ===== BRIEF.md ===== # Build brief · CVMatchScore The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a comparison between your CV and a job description, without inventing a score to replace CVMatchScore. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The premise this category sells is false, and you should know that before building it. The claim that around seventy five percent of applications are rejected by an applicant tracking system before a human sees them has no credible source. It traces back to a resume software vendor pitching it around 2012, which went out of business in 2013 without ever publishing a method. When recruiters are asked directly, the overwhelming majority say their systems do not automatically reject anyone: an applicant tracking system files and parses applications into searchable fields, and when parsing fails it routes the record to a human to fix rather than to the bin. So a tool that outputs a number out of a hundred, representing your chances against a gatekeeper that does not work that way, is selling anxiety. It is also unfalsifiable, since nobody can check the score against anything. What is genuinely useful is narrower and honest: show which requirements in the posting your CV does not evidence, quote where it does, and say plainly that the real reason applications go unanswered is usually that several hundred arrived in two days. That is what this builds. ### Stack (fixed, do not substitute) - Parsing: Structured extraction from both documents with positions kept · every claim the tool makes must be traceable to text in one of them - Requirements: Extracted from the posting as discrete, quoted items · a requirement is a sentence, not a keyword - Evidence: Matched to spans in the CV, or explicitly absent · absence is the useful output and must be stated as such - Refusal: No aggregate score anywhere in the model or interface · a number that cannot be checked will be believed anyway - Runtime: TypeScript with a local application · a CV and a salary history are personal and need not leave the machine ### Data model (create this before Phase 1) - documents(id, kind, text, filename, added_at) · kind is cv or posting - requirements(id, posting_id, quote, char_start, char_end, category, is_essential) · essential where the posting says so, never inferred - evidence(requirement_id, cv_id, quote, char_start, char_end, strength, note) - gaps(requirement_id, cv_id, kind, note) · kind is absent, weak or unclear - rewrites(id, cv_id, requirement_id, suggestion, accepted_at) · suggestions, never applied automatically - sessions(id, cv_id, posting_id, compared_at) - There is deliberately no score column anywhere, because a stored number would be displayed and it would be invented. ### Phase 1 · Reading both documents properly Build: Extract text from common CV formats with layout awareness, and from a posting pasted or fetched. Keep character positions so every later claim can quote. Report a document that could not be parsed rather than producing partial text. Trap: A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. Done when: A two-column CV extracts in correct reading order, verified by reading the output. Character positions are preserved so a quote can be located. A document that fails to parse is reported rather than producing partial text. A posting pasted as plain text works identically to a fetched one. Do not build yet: No requirements, matching or output. ### Phase 2 · Requirements as sentences, not keywords Build: Extract requirements from the posting as quoted items with their positions. Distinguish stated essentials from desirables using the posting's own language rather than inferring. Ignore boilerplate that is not a requirement. Trap: Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. Done when: Requirements are extracted as quoted sentences with positions rather than as keywords. Essentials are distinguished only where the posting says so, never inferred, verified against a posting that makes no such distinction. Boilerplate about company culture is not extracted as a requirement. A posting with vague requirements produces few rather than invented ones. Do not build yet: No matching or output yet. ### Phase 3 · Evidence, or its absence, stated plainly Build: Match each requirement to spans in the CV, quoting both sides. Where there is no evidence, say so explicitly rather than scoring it low. Distinguish absent from present but weakly evidenced. Trap: Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. Done when: Every requirement resolves to quoted CV evidence or an explicit absence. Weak evidence is distinguished from absence with both quoted. No percentage or partial-match number appears anywhere, verified by inspecting the output. A requirement the CV addresses in different words is matched and the difference is visible. Do not build yet: No suggestions or reporting yet. ### Phase 4 · Suggestions that do not write your CV Build: Suggest what a gap would need evidence of, phrased as a question about your experience rather than as text to insert. Never generate CV sentences. Let a suggestion be dismissed as not applicable. Trap: Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. Done when: Suggestions are phrased as questions about your experience rather than as insertable text. No generated CV sentence appears anywhere, verified by inspecting the output. A suggestion can be dismissed as not applicable and stays dismissed. Accepting a suggestion opens your own editor rather than inserting text. Do not build yet: No reporting or history yet. ### Phase 5 · Saying what the tool cannot tell you Build: Produce a report listing evidenced requirements, weak ones and absences with quotes, stating plainly what it does not know. Keep everything local. Track applications so you can see your own patterns rather than a predicted outcome. Trap: A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. Done when: The report states what the comparison does not tell you, prominently rather than in a footer. Evidenced, weak and absent requirements are listed with quotes from both documents. Everything runs locally, verified with networking disabled. Application tracking records what you sent and what happened without predicting anything. Do not build yet: Nothing after this. ### Out of scope (and why) - Any overall score or match percentage, which is the feature the category sells and which measures nothing real. - Predicting whether you will be contacted, which depends mostly on how many others applied. - Generating CV content, which invites claims about experience you do not have. - Optimising for a specific applicant tracking system, which is optimising against a system that does not reject you. ### README must contain - That the claim about most applications being auto-rejected has no credible source and traces to a defunct vendor - That applicant tracking systems file and parse applications rather than rejecting them - That no overall score exists anywhere in the model or the interface - That requirements are quoted sentences rather than keywords - That suggestions are questions about your experience rather than text to paste in ===== ARCHITECTURE.md ===== # Architecture · CVMatchScore ## Stack | Part | Choice | Why | | --- | --- | --- | | Parsing | Structured extraction from both documents with positions kept | every claim the tool makes must be traceable to text in one of them | | Requirements | Extracted from the posting as discrete, quoted items | a requirement is a sentence, not a keyword | | Evidence | Matched to spans in the CV, or explicitly absent | absence is the useful output and must be stated as such | | Refusal | No aggregate score anywhere in the model or interface | a number that cannot be checked will be believed anyway | | Runtime | TypeScript with a local application | a CV and a salary history are personal and need not leave the machine | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extraction | Reading both documents with positions. | Any extractor exposing positions. A plain text dump cannot support quoting. | | Requirements | Quoted sentences from the posting. | Any implementation, provided requirements stay sentences rather than keywords. | | Evidence | Matches and explicit absences. | Any matcher, provided absent and weak stay distinguishable. | | Suggestions | Questions about your experience. | Not swappable for generation, which invites claims you cannot support. | | Reporting | Findings and stated limits. | Any presentation, provided no aggregate verdict is implied. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Your CV and the jobs you are applying for. Personal in both directions. - `EXTRACTION_MODEL` · required · Runs over your CV and your job search. Local is the sensible default. - `EMIT_SCORE` · required · Present as a constant so the answer is visible. There is nothing real to score. - `GENERATE_CV_TEXT` · required · Present as a constant. A pasted claim becomes a lie at interview. - `ALLOW_NETWORK` · required · Present as a constant. Neither document needs to leave the machine. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · CVMatchScore product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Structured extraction from both documents with positions kept, Extracted from the posting as discrete, quoted items, Matched to spans in the CV, or explicitly absent, No aggregate score anywhere in the model or interface, TypeScript with a local application. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. - A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. ===== MILESTONES.md ===== # Delivery milestones · CVMatchScore Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading both documents properly A CV and a posting parse into text with positions preserved. ### Steps 1. Extract with layout awareness Designed CV templates are two-column and scramble under naive extraction. Files: `src/extract.ts` 2. Preserve character positions Every claim later has to quote, which needs positions. Files: `src/documents.ts` 3. Report parse failures Partial text produces a comparison that is confidently wrong. Files: `src/extract.ts` ### Done when - [ ] A two-column CV extracts in reading order - [ ] Character positions are preserved - [ ] Parse failures are reported - [ ] Pasted and fetched postings behave identically ### Watch out - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. ## M2 · Requirements as sentences, not keywords The posting yields discrete requirements you could argue with. ### Steps 1. Extract quoted sentences A keyword loses what was actually being asked for. Files: `src/requirements.ts` 2. Use the posting's own essential language Inferring which are essential is guessing at someone else's priorities. Files: `src/requirements.ts` 3. Skip boilerplate Culture paragraphs are not requirements and dilute the comparison. Files: `src/requirements.ts` ### Done when - [ ] Requirements are quoted sentences with positions - [ ] Essentials come from the posting's own language - [ ] Boilerplate is not extracted - [ ] A vague posting yields few requirements ### Watch out - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. ## M3 · Evidence, or its absence, stated plainly Each requirement is matched to CV text or reported as unevidenced. ### Steps 1. Quote both sides of every match So the judgement belongs to the person rather than the tool. Files: `src/match.ts` 2. Distinguish absent from weak They call for completely different responses. Files: `src/gaps.ts` 3. Emit no partial-match numbers A percentage hides exactly the distinction that matters. Files: `tests/no-scores.test.ts` ### Done when - [ ] Every requirement resolves to quoted evidence or explicit absence - [ ] Weak and absent are distinguished with quotes - [ ] No percentages appear anywhere - [ ] Differently worded matches are found and visible ### Watch out - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. ## M4 · Suggestions that do not write your CV You are told what is missing, not given sentences to paste in. ### Steps 1. Phrase gaps as questions What did you actually do, rather than here is what they wanted. Files: `src/suggest.ts` 2. Generate no CV text Its absence is the design. A pasted claim is a lie at interview. Files: `tests/no-generation.test.ts` 3. Keep dismissals Some requirements genuinely do not apply to you. Files: `src/suggest.ts` ### Done when - [ ] Suggestions are questions, not insertable text - [ ] No CV sentences are generated - [ ] Dismissals persist - [ ] Accepting opens your own editor ### Watch out - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ## M5 · Saying what the tool cannot tell you (production only) The output is honest about the limits of what a comparison shows. ### Steps 1. State the limits where they are read Not a footer. The reader is anxious and looking for a verdict. Files: `src/report.ts` 2. Keep it entirely local A CV and a job hunt are private in ways people underestimate. Files: `tests/offline.test.ts` 3. Track applications without predicting Your own patterns are real. A predicted outcome is not. Files: `src/applications.ts` ### Done when - [ ] The report states its limits prominently - [ ] Requirements are listed with quotes from both sides - [ ] Everything works with networking disabled - [ ] Tracking records outcomes without predicting them ### Watch out - A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. ===== OPERATIONS.md ===== # Operations · CVMatchScore ## Backup Your CV versions and application history are the data. Both are personal, and the application history in particular records rejections, so keep it somewhere you are comfortable with. ## Restore Restore and confirm dismissed suggestions survived, since regenerating without them re-raises every requirement you already decided does not apply to you. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the check is in the suite: no score field, no generated CV text, and everything working offline. All three are properties that erode by well-meaning addition rather than by breaking. ## Incident checklist A number appearing anywhere in the output is the failure this build is organised against, whether added deliberately or derived by a chart. The schema has no such field and that absence is the control. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A two-column CV is confirmed to extract in reading order - [ ] Requirements are confirmed to be quoted sentences - [ ] No score or percentage is confirmed present anywhere - [ ] No CV text is confirmed generated - [ ] Everything is confirmed to work with networking disabled ## Launch constraint Do not market omitted CVMatchScore capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your CV and the jobs you are applying for. Personal in both directions. DB_PATH=~/.local/share/cvmatch/data.db # Required. Runs over your CV and your job search. Local is the sensible default. EXTRACTION_MODEL=local # Required. Present as a constant so the answer is visible. There is nothing real to score. EMIT_SCORE=never # Required. Present as a constant. A pasted claim becomes a lie at interview. GENERATE_CV_TEXT=never # Required. Present as a constant. Neither document needs to leave the machine. ALLOW_NETWORK=never
# CVMatchScore · indie build A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Parsing | Structured extraction from both documents with positions kept | every claim the tool makes must be traceable to text in one of them | | Requirements | Extracted from the posting as discrete, quoted items | a requirement is a sentence, not a keyword | | Evidence | Matched to spans in the CV, or explicitly absent | absence is the useful output and must be stated as such | | Refusal | No aggregate score anywhere in the model or interface | a number that cannot be checked will be believed anyway | | Runtime | TypeScript with a local application | a CV and a salary history are personal and need not leave the machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A layout-aware document extractor** · free - Why: Designed CV templates are two-column and a naive extractor produces interleaved text that every later step then operates on. - Get it: A library exposing text with positions and block structure. - Verify: A two-column CV extracts in correct reading order - [ ] **A local model for extraction** · free, needs memory - Why: A CV and the jobs someone is applying for are private in ways people underestimate, and this runs over both. - Get it: Any local runner with a model that returns structured output reliably. - Verify: Ten extractions return valid structured output with positions - [ ] **Your own CV and ten real postings** · free - Why: Requirement extraction quality is only assessable against real postings, which vary from precise lists to three paragraphs of aspiration. - Get it: Collect postings you would actually apply for, including a vague one and one heavy with boilerplate. - Verify: Ten postings exist including one vague and one boilerplate-heavy - [ ] **What you will tell users about their chances** · free - Why: The whole build refuses to predict, and the wording of that refusal is the most important text in the interface. - Get it: Write the sentence the report leads with about what the comparison does and does not tell them. - Verify: A written sentence exists and appears where the reader will see it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. - a calibrated rubric that scores the same resume the same way twice - 50+ language support tested per parameter - DOC, DOCX, and RTF parsing beyond PDF - improvement plans and tailored cover letters built from the same analysis - scores comparable across weeks of applications If one of those is essential to you, that is the reason to keep paying for CVMatchScore, and the README should say so rather than pretend.
# Build brief · CVMatchScore The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a comparison between your CV and a job description, without inventing a score to replace CVMatchScore. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The premise this category sells is false, and you should know that before building it. The claim that around seventy five percent of applications are rejected by an applicant tracking system before a human sees them has no credible source. It traces back to a resume software vendor pitching it around 2012, which went out of business in 2013 without ever publishing a method. When recruiters are asked directly, the overwhelming majority say their systems do not automatically reject anyone: an applicant tracking system files and parses applications into searchable fields, and when parsing fails it routes the record to a human to fix rather than to the bin. So a tool that outputs a number out of a hundred, representing your chances against a gatekeeper that does not work that way, is selling anxiety. It is also unfalsifiable, since nobody can check the score against anything. What is genuinely useful is narrower and honest: show which requirements in the posting your CV does not evidence, quote where it does, and say plainly that the real reason applications go unanswered is usually that several hundred arrived in two days. That is what this builds. ### Stack (fixed, do not substitute) - Parsing: Structured extraction from both documents with positions kept · every claim the tool makes must be traceable to text in one of them - Requirements: Extracted from the posting as discrete, quoted items · a requirement is a sentence, not a keyword - Evidence: Matched to spans in the CV, or explicitly absent · absence is the useful output and must be stated as such - Refusal: No aggregate score anywhere in the model or interface · a number that cannot be checked will be believed anyway - Runtime: TypeScript with a local application · a CV and a salary history are personal and need not leave the machine ### Data model (create this before Phase 1) - documents(id, kind, text, filename, added_at) · kind is cv or posting - requirements(id, posting_id, quote, char_start, char_end, category, is_essential) · essential where the posting says so, never inferred - evidence(requirement_id, cv_id, quote, char_start, char_end, strength, note) - gaps(requirement_id, cv_id, kind, note) · kind is absent, weak or unclear - rewrites(id, cv_id, requirement_id, suggestion, accepted_at) · suggestions, never applied automatically - sessions(id, cv_id, posting_id, compared_at) - There is deliberately no score column anywhere, because a stored number would be displayed and it would be invented. ### Phase 1 · Reading both documents properly Build: Extract text from common CV formats with layout awareness, and from a posting pasted or fetched. Keep character positions so every later claim can quote. Report a document that could not be parsed rather than producing partial text. Trap: A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. Done when: A two-column CV extracts in correct reading order, verified by reading the output. Character positions are preserved so a quote can be located. A document that fails to parse is reported rather than producing partial text. A posting pasted as plain text works identically to a fetched one. Do not build yet: No requirements, matching or output. ### Phase 2 · Requirements as sentences, not keywords Build: Extract requirements from the posting as quoted items with their positions. Distinguish stated essentials from desirables using the posting's own language rather than inferring. Ignore boilerplate that is not a requirement. Trap: Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. Done when: Requirements are extracted as quoted sentences with positions rather than as keywords. Essentials are distinguished only where the posting says so, never inferred, verified against a posting that makes no such distinction. Boilerplate about company culture is not extracted as a requirement. A posting with vague requirements produces few rather than invented ones. Do not build yet: No matching or output yet. ### Phase 3 · Evidence, or its absence, stated plainly Build: Match each requirement to spans in the CV, quoting both sides. Where there is no evidence, say so explicitly rather than scoring it low. Distinguish absent from present but weakly evidenced. Trap: Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. Done when: Every requirement resolves to quoted CV evidence or an explicit absence. Weak evidence is distinguished from absence with both quoted. No percentage or partial-match number appears anywhere, verified by inspecting the output. A requirement the CV addresses in different words is matched and the difference is visible. Do not build yet: No suggestions or reporting yet. ### Phase 4 · Suggestions that do not write your CV Build: Suggest what a gap would need evidence of, phrased as a question about your experience rather than as text to insert. Never generate CV sentences. Let a suggestion be dismissed as not applicable. Trap: Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. Done when: Suggestions are phrased as questions about your experience rather than as insertable text. No generated CV sentence appears anywhere, verified by inspecting the output. A suggestion can be dismissed as not applicable and stays dismissed. Accepting a suggestion opens your own editor rather than inserting text. Do not build yet: No reporting or history yet. ### Phase 5 · Saying what the tool cannot tell you Build: Produce a report listing evidenced requirements, weak ones and absences with quotes, stating plainly what it does not know. Keep everything local. Track applications so you can see your own patterns rather than a predicted outcome. Trap: A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. Done when: The report states what the comparison does not tell you, prominently rather than in a footer. Evidenced, weak and absent requirements are listed with quotes from both documents. Everything runs locally, verified with networking disabled. Application tracking records what you sent and what happened without predicting anything. Do not build yet: Nothing after this. ### Out of scope (and why) - Any overall score or match percentage, which is the feature the category sells and which measures nothing real. - Predicting whether you will be contacted, which depends mostly on how many others applied. - Generating CV content, which invites claims about experience you do not have. - Optimising for a specific applicant tracking system, which is optimising against a system that does not reject you. ### README must contain - That the claim about most applications being auto-rejected has no credible source and traces to a defunct vendor - That applicant tracking systems file and parse applications rather than rejecting them - That no overall score exists anywhere in the model or the interface - That requirements are quoted sentences rather than keywords - That suggestions are questions about your experience rather than text to paste in
# Agent instructions · CVMatchScore indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Structured extraction from both documents with positions kept, Extracted from the posting as discrete, quoted items, Matched to spans in the CV, or explicitly absent, No aggregate score anywhere in the model or interface, TypeScript with a local application. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear.
# Build plan · CVMatchScore A comparison tool that maps a job description's stated requirements against evidence in a CV, quoting both sides, and refuses to produce an overall score because there is nothing real for a score to measure. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading both documents properly A CV and a posting parse into text with positions preserved. ### Steps 1. Extract with layout awareness Designed CV templates are two-column and scramble under naive extraction. Files: `src/extract.ts` 2. Preserve character positions Every claim later has to quote, which needs positions. Files: `src/documents.ts` 3. Report parse failures Partial text produces a comparison that is confidently wrong. Files: `src/extract.ts` ### Done when - [ ] A two-column CV extracts in reading order - [ ] Character positions are preserved - [ ] Parse failures are reported - [ ] Pasted and fetched postings behave identically ### Watch out - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. ## Phase 2 · Requirements as sentences, not keywords The posting yields discrete requirements you could argue with. ### Steps 1. Extract quoted sentences A keyword loses what was actually being asked for. Files: `src/requirements.ts` 2. Use the posting's own essential language Inferring which are essential is guessing at someone else's priorities. Files: `src/requirements.ts` 3. Skip boilerplate Culture paragraphs are not requirements and dilute the comparison. Files: `src/requirements.ts` ### Done when - [ ] Requirements are quoted sentences with positions - [ ] Essentials come from the posting's own language - [ ] Boilerplate is not extracted - [ ] A vague posting yields few requirements ### Watch out - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. ## Phase 3 · Evidence, or its absence, stated plainly Each requirement is matched to CV text or reported as unevidenced. ### Steps 1. Quote both sides of every match So the judgement belongs to the person rather than the tool. Files: `src/match.ts` 2. Distinguish absent from weak They call for completely different responses. Files: `src/gaps.ts` 3. Emit no partial-match numbers A percentage hides exactly the distinction that matters. Files: `tests/no-scores.test.ts` ### Done when - [ ] Every requirement resolves to quoted evidence or explicit absence - [ ] Weak and absent are distinguished with quotes - [ ] No percentages appear anywhere - [ ] Differently worded matches are found and visible ### Watch out - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. ## Phase 4 · Suggestions that do not write your CV You are told what is missing, not given sentences to paste in. ### Steps 1. Phrase gaps as questions What did you actually do, rather than here is what they wanted. Files: `src/suggest.ts` 2. Generate no CV text Its absence is the design. A pasted claim is a lie at interview. Files: `tests/no-generation.test.ts` 3. Keep dismissals Some requirements genuinely do not apply to you. Files: `src/suggest.ts` ### Done when - [ ] Suggestions are questions, not insertable text - [ ] No CV sentences are generated - [ ] Dismissals persist - [ ] Accepting opens your own editor ### Watch out - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ## Not in this build - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. ## After v1, if you want it - Comparing one CV against several postings to see which requirements recur - Tracking which of your own claims get asked about at interview, which is real feedback - A version history of your CV, since most people keep six files and lose track
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your CV and the jobs you are applying for. Personal in both directions. DB_PATH=~/.local/share/cvmatch/data.db # Required. Runs over your CV and your job search. Local is the sensible default. EXTRACTION_MODEL=local # Required. Present as a constant so the answer is visible. There is nothing real to score. EMIT_SCORE=never # Required. Present as a constant. A pasted claim becomes a lie at interview. GENERATE_CV_TEXT=never # Required. Present as a constant. Neither document needs to leave the machine. ALLOW_NETWORK=never
# CVMatchScore · product brief ## Problem The core loop, resume plus job posting into an LLM holding a scoring rubric, is one prompt and an afternoon, and for improving one resume against one posting it genuinely works. The honest gap is calibration: a rubric you wrote today measures today's mood, two runs of the same resume can disagree, and a 72 means nothing without a baseline of scored applications behind it. Fine as a mirror, thin as a measuring stick. ## Product outcome A comparison that shows exactly which stated requirements your CV does not evidence, quotes both sides, and refuses to invent a number about your chances. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - OpenAI or Anthropic API key - pdf-parse or pdfplumber for resume text extraction - a written rubric with a 0-10 definition per criterion - a few real resume and posting pairs to sanity-check the scores ## Explicit non-goals for v1 - Any overall score or match percentage. - Predicting whether you will be contacted. - Generating CV content. - Optimising for a specific applicant tracking system. - a calibrated rubric that scores the same resume the same way twice - 50+ language support tested per parameter - DOC, DOCX, and RTF parsing beyond PDF - improvement plans and tailored cover letters built from the same analysis - scores comparable across weeks of applications ## Success criteria - A two-column CV is confirmed to extract in reading order - Requirements are confirmed to be quoted sentences - No score or percentage is confirmed present anywhere - No CV text is confirmed generated - Everything is confirmed to work with networking disabled
# Build brief · CVMatchScore The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a comparison between your CV and a job description, without inventing a score to replace CVMatchScore. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The premise this category sells is false, and you should know that before building it. The claim that around seventy five percent of applications are rejected by an applicant tracking system before a human sees them has no credible source. It traces back to a resume software vendor pitching it around 2012, which went out of business in 2013 without ever publishing a method. When recruiters are asked directly, the overwhelming majority say their systems do not automatically reject anyone: an applicant tracking system files and parses applications into searchable fields, and when parsing fails it routes the record to a human to fix rather than to the bin. So a tool that outputs a number out of a hundred, representing your chances against a gatekeeper that does not work that way, is selling anxiety. It is also unfalsifiable, since nobody can check the score against anything. What is genuinely useful is narrower and honest: show which requirements in the posting your CV does not evidence, quote where it does, and say plainly that the real reason applications go unanswered is usually that several hundred arrived in two days. That is what this builds. ### Stack (fixed, do not substitute) - Parsing: Structured extraction from both documents with positions kept · every claim the tool makes must be traceable to text in one of them - Requirements: Extracted from the posting as discrete, quoted items · a requirement is a sentence, not a keyword - Evidence: Matched to spans in the CV, or explicitly absent · absence is the useful output and must be stated as such - Refusal: No aggregate score anywhere in the model or interface · a number that cannot be checked will be believed anyway - Runtime: TypeScript with a local application · a CV and a salary history are personal and need not leave the machine ### Data model (create this before Phase 1) - documents(id, kind, text, filename, added_at) · kind is cv or posting - requirements(id, posting_id, quote, char_start, char_end, category, is_essential) · essential where the posting says so, never inferred - evidence(requirement_id, cv_id, quote, char_start, char_end, strength, note) - gaps(requirement_id, cv_id, kind, note) · kind is absent, weak or unclear - rewrites(id, cv_id, requirement_id, suggestion, accepted_at) · suggestions, never applied automatically - sessions(id, cv_id, posting_id, compared_at) - There is deliberately no score column anywhere, because a stored number would be displayed and it would be invented. ### Phase 1 · Reading both documents properly Build: Extract text from common CV formats with layout awareness, and from a posting pasted or fetched. Keep character positions so every later claim can quote. Report a document that could not be parsed rather than producing partial text. Trap: A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. Done when: A two-column CV extracts in correct reading order, verified by reading the output. Character positions are preserved so a quote can be located. A document that fails to parse is reported rather than producing partial text. A posting pasted as plain text works identically to a fetched one. Do not build yet: No requirements, matching or output. ### Phase 2 · Requirements as sentences, not keywords Build: Extract requirements from the posting as quoted items with their positions. Distinguish stated essentials from desirables using the posting's own language rather than inferring. Ignore boilerplate that is not a requirement. Trap: Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. Done when: Requirements are extracted as quoted sentences with positions rather than as keywords. Essentials are distinguished only where the posting says so, never inferred, verified against a posting that makes no such distinction. Boilerplate about company culture is not extracted as a requirement. A posting with vague requirements produces few rather than invented ones. Do not build yet: No matching or output yet. ### Phase 3 · Evidence, or its absence, stated plainly Build: Match each requirement to spans in the CV, quoting both sides. Where there is no evidence, say so explicitly rather than scoring it low. Distinguish absent from present but weakly evidenced. Trap: Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. Done when: Every requirement resolves to quoted CV evidence or an explicit absence. Weak evidence is distinguished from absence with both quoted. No percentage or partial-match number appears anywhere, verified by inspecting the output. A requirement the CV addresses in different words is matched and the difference is visible. Do not build yet: No suggestions or reporting yet. ### Phase 4 · Suggestions that do not write your CV Build: Suggest what a gap would need evidence of, phrased as a question about your experience rather than as text to insert. Never generate CV sentences. Let a suggestion be dismissed as not applicable. Trap: Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. Done when: Suggestions are phrased as questions about your experience rather than as insertable text. No generated CV sentence appears anywhere, verified by inspecting the output. A suggestion can be dismissed as not applicable and stays dismissed. Accepting a suggestion opens your own editor rather than inserting text. Do not build yet: No reporting or history yet. ### Phase 5 · Saying what the tool cannot tell you Build: Produce a report listing evidenced requirements, weak ones and absences with quotes, stating plainly what it does not know. Keep everything local. Track applications so you can see your own patterns rather than a predicted outcome. Trap: A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer. Done when: The report states what the comparison does not tell you, prominently rather than in a footer. Evidenced, weak and absent requirements are listed with quotes from both documents. Everything runs locally, verified with networking disabled. Application tracking records what you sent and what happened without predicting anything. Do not build yet: Nothing after this. ### Out of scope (and why) - Any overall score or match percentage, which is the feature the category sells and which measures nothing real. - Predicting whether you will be contacted, which depends mostly on how many others applied. - Generating CV content, which invites claims about experience you do not have. - Optimising for a specific applicant tracking system, which is optimising against a system that does not reject you. ### README must contain - That the claim about most applications being auto-rejected has no credible source and traces to a defunct vendor - That applicant tracking systems file and parse applications rather than rejecting them - That no overall score exists anywhere in the model or the interface - That requirements are quoted sentences rather than keywords - That suggestions are questions about your experience rather than text to paste in
# Architecture · CVMatchScore ## Stack | Part | Choice | Why | | --- | --- | --- | | Parsing | Structured extraction from both documents with positions kept | every claim the tool makes must be traceable to text in one of them | | Requirements | Extracted from the posting as discrete, quoted items | a requirement is a sentence, not a keyword | | Evidence | Matched to spans in the CV, or explicitly absent | absence is the useful output and must be stated as such | | Refusal | No aggregate score anywhere in the model or interface | a number that cannot be checked will be believed anyway | | Runtime | TypeScript with a local application | a CV and a salary history are personal and need not leave the machine | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extraction | Reading both documents with positions. | Any extractor exposing positions. A plain text dump cannot support quoting. | | Requirements | Quoted sentences from the posting. | Any implementation, provided requirements stay sentences rather than keywords. | | Evidence | Matches and explicit absences. | Any matcher, provided absent and weak stay distinguishable. | | Suggestions | Questions about your experience. | Not swappable for generation, which invites claims you cannot support. | | Reporting | Findings and stated limits. | Any presentation, provided no aggregate verdict is implied. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Your CV and the jobs you are applying for. Personal in both directions. - `EXTRACTION_MODEL` · required · Runs over your CV and your job search. Local is the sensible default. - `EMIT_SCORE` · required · Present as a constant so the answer is visible. There is nothing real to score. - `GENERATE_CV_TEXT` · required · Present as a constant. A pasted claim becomes a lie at interview. - `ALLOW_NETWORK` · required · Present as a constant. Neither document needs to leave the machine. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · CVMatchScore product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Structured extraction from both documents with positions kept, Extracted from the posting as discrete, quoted items, Matched to spans in the CV, or explicitly absent, No aggregate score anywhere in the model or interface, TypeScript with a local application. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. - A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer.
# Delivery milestones · CVMatchScore Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading both documents properly A CV and a posting parse into text with positions preserved. ### Steps 1. Extract with layout awareness Designed CV templates are two-column and scramble under naive extraction. Files: `src/extract.ts` 2. Preserve character positions Every claim later has to quote, which needs positions. Files: `src/documents.ts` 3. Report parse failures Partial text produces a comparison that is confidently wrong. Files: `src/extract.ts` ### Done when - [ ] A two-column CV extracts in reading order - [ ] Character positions are preserved - [ ] Parse failures are reported - [ ] Pasted and fetched postings behave identically ### Watch out - A CV in two columns, which is common in designed templates, extracts as interleaved nonsense with a naive reader, and every comparison after that operates on scrambled text while looking entirely plausible. Reconstruct reading order and check the output is readable before doing anything with it. ## M2 · Requirements as sentences, not keywords The posting yields discrete requirements you could argue with. ### Steps 1. Extract quoted sentences A keyword loses what was actually being asked for. Files: `src/requirements.ts` 2. Use the posting's own essential language Inferring which are essential is guessing at someone else's priorities. Files: `src/requirements.ts` 3. Skip boilerplate Culture paragraphs are not requirements and dilute the comparison. Files: `src/requirements.ts` ### Done when - [ ] Requirements are quoted sentences with positions - [ ] Essentials come from the posting's own language - [ ] Boilerplate is not extracted - [ ] A vague posting yields few requirements ### Watch out - Reducing a posting to a keyword list turns a requirement like experience leading a small team through a migration into the word migration, and then matching becomes a word game rather than a comparison. Keep requirements as quoted sentences so the user can see what is actually being asked for. ## M3 · Evidence, or its absence, stated plainly Each requirement is matched to CV text or reported as unevidenced. ### Steps 1. Quote both sides of every match So the judgement belongs to the person rather than the tool. Files: `src/match.ts` 2. Distinguish absent from weak They call for completely different responses. Files: `src/gaps.ts` 3. Emit no partial-match numbers A percentage hides exactly the distinction that matters. Files: `tests/no-scores.test.ts` ### Done when - [ ] Every requirement resolves to quoted evidence or explicit absence - [ ] Weak and absent are distinguished with quotes - [ ] No percentages appear anywhere - [ ] Differently worded matches are found and visible ### Watch out - Reporting a partial match as a percentage hides whether the CV said something adjacent or nothing at all, and those need completely different responses from the person. Say absent, weak or evidenced, and quote the text in every case so the judgement is theirs. ## M4 · Suggestions that do not write your CV You are told what is missing, not given sentences to paste in. ### Steps 1. Phrase gaps as questions What did you actually do, rather than here is what they wanted. Files: `src/suggest.ts` 2. Generate no CV text Its absence is the design. A pasted claim is a lie at interview. Files: `tests/no-generation.test.ts` 3. Keep dismissals Some requirements genuinely do not apply to you. Files: `src/suggest.ts` ### Done when - [ ] Suggestions are questions, not insertable text - [ ] No CV sentences are generated - [ ] Dismissals persist - [ ] Accepting opens your own editor ### Watch out - Generating a bullet point for a gap invites someone to paste in a claim about experience they do not have, which is a lie on a CV with their name on it and is discovered at interview. Ask what they actually did rather than supplying what the posting wanted to hear. ## M5 · Saying what the tool cannot tell you (production only) The output is honest about the limits of what a comparison shows. ### Steps 1. State the limits where they are read Not a footer. The reader is anxious and looking for a verdict. Files: `src/report.ts` 2. Keep it entirely local A CV and a job hunt are private in ways people underestimate. Files: `tests/offline.test.ts` 3. Track applications without predicting Your own patterns are real. A predicted outcome is not. Files: `src/applications.ts` ### Done when - [ ] The report states its limits prominently - [ ] Requirements are listed with quotes from both sides - [ ] Everything works with networking disabled - [ ] Tracking records outcomes without predicting them ### Watch out - A report that ends with an implied verdict, even without a number, reads as a prediction, and the honest position is that the tool has no idea whether you will hear back because the dominant factor is how many other people applied. State that where the reader will see it rather than in a footer.
# Operations · CVMatchScore ## Backup Your CV versions and application history are the data. Both are personal, and the application history in particular records rejections, so keep it somewhere you are comfortable with. ## Restore Restore and confirm dismissed suggestions survived, since regenerating without them re-raises every requirement you already decided does not apply to you. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the check is in the suite: no score field, no generated CV text, and everything working offline. All three are properties that erode by well-meaning addition rather than by breaking. ## Incident checklist A number appearing anywhere in the output is the failure this build is organised against, whether added deliberately or derived by a chart. The schema has no such field and that absence is the control. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A two-column CV is confirmed to extract in reading order - [ ] Requirements are confirmed to be quoted sentences - [ ] No score or percentage is confirmed present anywhere - [ ] No CV text is confirmed generated - [ ] Everything is confirmed to work with networking disabled ## Launch constraint Do not market omitted CVMatchScore capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your CV and the jobs you are applying for. Personal in both directions. DB_PATH=~/.local/share/cvmatch/data.db # Required. Runs over your CV and your job search. Local is the sensible default. EXTRACTION_MODEL=local # Required. Present as a constant so the answer is visible. There is nothing real to score. EMIT_SCORE=never # Required. Present as a constant. A pasted claim becomes a lie at interview. GENERATE_CV_TEXT=never # Required. Present as a constant. Neither document needs to leave the machine. ALLOW_NETWORK=never
$ choose a build depth, inspect the files, then open the complete pack in your agent
At 49 USD a year it is priced below the hassle of maintaining your own: job seekers pay for stable scores they can track across applications, cover letters generated from the same pass, and not burning API credits mid job hunt.
xa calibrated rubric that scores the same resume the same way twice
x50+ language support tested per parameter
xDOC, DOCX, and RTF parsing beyond PDF
ximprovement plans and tailored cover letters built from the same analysis
xscores comparable across weeks of applications
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
CVMatchScore pricing
pro$4.08/mo · yearly, converted to monthly · $48.96/yr
free tier3 full reports in the first 7 days, all 19 parameters, no credit card.
verified 2026-08-10 · source ↗
Is CVMatchScore free?
3 full reports in the first 7 days, all 19 parameters, no credit card. Paid is PRO at $4.08/mo (checked 2026-08-10).
Vibecode CVMatchScore
Kinda. The core of CVMatchScore is buildable in a weekend with the prompt on this page, but there are real gaps: a calibrated rubric that scores the same resume the same way twice, 50+ language support tested per parameter. Read the honest list above before committing.
How much does CVMatchScore cost?
CVMatchScore costs about $4.08/month (PRO, checked 2026-08-10), which is $48.96 per year.
What do I lose by replacing CVMatchScore?
Honestly: a calibrated rubric that scores the same resume the same way twice; 50+ language support tested per parameter; DOC, DOCX, and RTF parsing beyond PDF; improvement plans and tailored cover letters built from the same analysis; scores comparable across weeks of applications. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to CVMatchScore?
Yes: Resume-Matcher (A local resume-vs-posting matcher that runs against Ollama, so the scoring stays on your machine; you install it, it does the job, and nobody bills you.) The prompt is for when you want it exactly your way.