Vibecode Enhancv
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Enhancv, create visually polished resume variants from structured user evidence. The hard boundary is template design, content guidance, scoring, document rendering, and support, plus data, distribution, and coaching.
You are building a lean indie version of Enhancv. 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 ===== # Enhancv · indie build One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. 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 | | --- | --- | --- | | Runtime | TypeScript with Vite, running locally | your employment history is private and there is no reason for it to need a server | | Source | One structured record, several outputs | the record is the asset and every document is derived from it | | Layout | Computed from content, not positioned by hand | a resume reflows when you add a role, and hand positioning breaks the moment it does | | Output | PDF with real text, generated from the same source for both versions | an image of text parses as nothing at all | | Verification | Extract text from your own output and check the order | the only way to know whether the document parses is to parse it | ## 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 build and the PDF generation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A PDF text extraction tool** · free - Why: Phase 2's check is the whole point and it cannot be done by looking at the page. You need to see the text in the order a machine reads it. - Get it: poppler utilities provide a text extractor, or use any library that reports extraction order. - Verify: Extracting a two column PDF visibly interleaves its columns - [ ] **An honest account of your own experience** · free, and it is the most valuable hour here - Why: Everything is derived from it, and it cannot be generated. Roles, dates, projects and metrics you could defend. - Get it: Set aside an hour and write it from memory and records rather than from an existing resume. - Verify: Every role you would mention has an entry with real dates - [ ] **Three real job descriptions** · free - Why: Tailoring and relevance ranking cannot be judged against invented postings, and real ones contain the vague requirements that break naive matching. - Get it: Save three for roles you would actually apply for. - Verify: Three descriptions are stored as fixtures - [ ] **A model API key, or a local model** (optional) · pay per token; tailoring one document is a fraction of a cent - Why: Phase 4 only. The record, the templates and the extraction checks all work without one. - Get it: A provider key with a spend limit, or a local model if you would rather your employment history stayed on the machine. - Verify: A tailored document generates and passes validation ## Quick start ```sh pdftotext -layout plain.pdf - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. - template design, content guidance, scoring, document rendering, and support - proprietary recruiter data - job-board distribution - human coaching - automated application networks If one of those is essential to you, that is the reason to keep paying for Enhancv, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Enhancv 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 resume builder producing documents that look designed and still parse correctly to replace Enhancv. 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 There is a tension at the centre of this and pretending otherwise produces a bad tool. A heavily designed resume, with columns, sidebars, icons and text in graphics, reads well to a person and parses badly to the software that reads it first. A plain single-column document parses reliably and looks unremarkable. So this produces both from one source: a machine-readable version whose text extracts in the right order, and a designed version for a human. It tells you which to send where, and it never lets you produce only the pretty one by accident. The content rule from the start: every claim traces to your own record of what you did. A resume that overstates is a conversation you cannot win in an interview. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running locally · your employment history is private and there is no reason for it to need a server - Source: One structured record, several outputs · the record is the asset and every document is derived from it - Layout: Computed from content, not positioned by hand · a resume reflows when you add a role, and hand positioning breaks the moment it does - Output: PDF with real text, generated from the same source for both versions · an image of text parses as nothing at all - Verification: Extract text from your own output and check the order · the only way to know whether the document parses is to parse it ### Data model (create this before Phase 1) - profile(id, name, contact_json, summary) - entries(id, kind, title, organisation, started_on, ended_on, location, bullets_json, metrics_json) · kind is role, project, education, certification - skills(id, name, category, evidence_entry_ids_json) · a skill with no supporting entry is a claim with no basis - documents(id, name, template_key, target_role, entry_ids_json, generated_at, pdf_path) - generated_lines(document_id, line, source_entry_id, source_kind) · every line traces to an entry - extraction_checks(document_id, at, extracted_text, order_ok, notes) - Nothing appears on a document that does not reference an entry in the record. ### Phase 1 · The record, written once Build: Structured entries for roles, projects, education and certifications, with real dates and any metrics you can support. Skills referencing the entries that evidence them. Write it from what you did rather than from an existing resume. Trap: Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. Done when: Every role and project you would mention has an entry with real dates. A skill with no supporting entry is flagged rather than accepted. Metrics are real figures or empty, never estimates. The record exports as readable Markdown. Do not build yet: No documents, layout or tailoring. ### Phase 2 · A document that parses correctly Build: A single column template with real text, standard section headings and no text inside graphics. Generate a PDF, then extract its text with a tool and confirm the order and the section headings survive. Trap: Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. Trap: Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. Done when: Extracted text from the plain version comes out in reading order, verified with an extraction tool rather than by eye. Section headings appear as text. Contact details are real text, not graphics. Dates extract in a consistent, parseable form. Do not build yet: No designed version or tailoring yet. ### Phase 3 · The designed version, and saying which is which Build: A designed template with columns and visual hierarchy, generated from the same record. Run the same extraction check and report honestly that it parses worse. Label each output with what it is for. Trap: Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. Done when: Both versions generate from one record. The extraction check runs on both and reports the difference. Each output is labelled with its intended use. The plain version is the default for upload, and choosing the designed one for upload produces a warning. Do not build yet: No tailoring yet. ### Phase 4 · Tailoring that cannot invent Build: Given a job description, select and order the entries that fit and rephrase bullets within the meaning of the original. Record the source entry for every generated line. Validate that each line maps to an entry, and reject the document if one does not. Trap: A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. Done when: A tailored document shows the source entry for every line. A crafted document containing an unsupported line fails validation, tested by feeding one to the validator directly. Reordering by relevance to a description changes which entries appear first. Nothing appears that is not in the record. Do not build yet: No length management or export yet. ### Phase 5 · Length, which is a decision rather than a font size Build: Report when content exceeds the target page count and show what could be removed, ranked by relevance to the target role. Enforce a minimum type size below which the document reports overflow rather than shrinking further. Trap: Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. Done when: Content exceeding the page target produces a warning naming the overflow rather than shrinking below the floor. Suggested cuts are ranked by relevance to the target role. Removing a suggested entry brings the document within the target. Type size never falls below the configured floor. Do not build yet: Export and application tracking come last. ### Phase 6 · Versions, records and what you sent where Build: Keep every generated document with its record snapshot and target role. Export the whole thing. Then verify the plain version against a real parsing check by uploading it somewhere that reports what it extracted. Trap: Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. Done when: Every generated document is kept with its record snapshot and target. Searching by company returns the document sent. Export produces all documents and the record. The plain version has been checked against a real parser and the result recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Template design at the level of the commercial product, which is professional design work rather than code. - Content guidance and scoring against what recruiters respond to, which requires data you do not have. - Any claim that a resume will pass a particular system, since systems differ and change and none of them publish their rules. - Distribution to job boards, which is an integration per board. ### README must contain - That designed layouts parse worse than plain ones, and that both are generated - That the plain version is the default for anything uploaded - That every line on a tailored document traces to an entry in your record - That overflow is reported rather than fixed by shrinking type - That every generated document is kept with the record as it was at the time ===== AGENTS.md ===== # Agent instructions · Enhancv indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running locally, One structured record, several outputs, Computed from content, not positioned by hand, PDF with real text, generated from the same source for both versions, Extract text from your own output and check the order. 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 - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ===== BUILD_PLAN.md ===== # Build plan · Enhancv One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The record, written once Everything you might claim exists in one place, in your own words. ### Steps 1. Model entries with dates and bullets Real dates, your own wording, metrics only where you can support them. Files: `src/record.ts` 2. Require evidence for skills A skill references the entries that demonstrate it. Unsupported skills are flagged. Files: `src/skills.ts` 3. Export the record Readable Markdown, so it outlives this tool. Files: `src/export.ts` ### Done when - [ ] Every role and project has an entry with real dates - [ ] An unsupported skill is flagged - [ ] Metrics are real or empty - [ ] The record exports as readable Markdown ### Watch out - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. ## Phase 2 · A document that parses correctly The plain version's extracted text comes out in reading order. ### Steps 1. Build the plain template single column Standard headings, real text, no graphics carrying information. Files: `src/templates/plain.ts` 2. Extract and check the order With a text extraction tool, as an automated check rather than a manual one. Files: `src/verify-extraction.ts` ```sh pdftotext -layout plain.pdf - ``` 3. Make the check part of generation Generating a document runs the extraction check and reports the result. Files: `src/generate.ts` ### Done when - [ ] Extracted text is in reading order - [ ] Section headings appear as text - [ ] Contact details are text rather than graphics - [ ] The extraction check runs as part of generation ### Watch out - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. ## Phase 3 · The designed version, and saying which is which You get a document that looks good, and you are told where to send each version. ### Steps 1. Build the designed template From the same record. Columns and hierarchy, accepting the extraction cost. Files: `src/templates/designed.ts` 2. Report both extraction results Side by side. The comparison is the information the user needs. Files: `src/verify-extraction.ts` 3. Default to plain for upload With a warning if the designed one is chosen for a system that will parse it. Files: `src/generate.ts` ### Done when - [ ] Both versions generate from one record - [ ] Extraction results are reported for both - [ ] Each output is labelled with its use - [ ] Choosing the designed version for upload warns ### Watch out - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. ## Phase 4 · Tailoring that cannot invent A version targeted at a role rearranges what you did and adds nothing. ### Steps 1. Select and order by relevance Against the description. Selection is most of tailoring and needs no model. Files: `src/tailor.ts` 2. Rephrase within meaning only Constrained to the bullet's own content. Ask for structured output naming the source entry. Files: `src/rephrase.ts` 3. Validate every line A pure function, unit tested with a crafted unsupported line. Files: `src/validate.ts`, `src/validate.test.ts` ### Done when - [ ] Every line shows its source entry - [ ] A crafted unsupported line is rejected - [ ] Relevance ordering changes with the description - [ ] Nothing appears outside the record ### Watch out - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. ## Phase 5 · Length, which is a decision rather than a font size Fitting a page means cutting something, not shrinking the type. ### Steps 1. Set a readable floor Below it the document reports overflow. This is the rule that keeps output readable. Files: `src/layout.ts` 2. Rank suggested cuts Least relevant to the target role first. The suggestion is the useful part. Files: `src/trim.ts` 3. Verify by cutting Removing a suggestion should actually bring it within the target. ### Done when - [ ] Overflow warns rather than shrinking below the floor - [ ] Suggested cuts are ranked by relevance - [ ] Removing a suggestion brings it within target - [ ] Type size never falls below the floor ### Watch out - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ## Not in this build - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. ## After v1, if you want it - A cover letter generated from the same record with the same validation - A version comparison showing what changed between two tailored documents - A plain text output for forms that ask you to paste rather than upload ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your experience record. Private, and worth backing up. RECORD_PATH=./data/record.json # Required. Generated PDFs with their record snapshots. DOCUMENTS_DIR=./data/documents # Required. Below this the layout reports overflow instead of shrinking. MIN_FONT_PT=9.5 # Required. Page target. Exceeding it suggests cuts rather than shrinking type. TARGET_PAGES=2 # Required. Which adapter tailoring uses. Local means your history stays on the machine. MODEL_PROVIDER=local
You are building a lean indie version of Enhancv. 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 ===== # Enhancv · indie build One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. 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 | | --- | --- | --- | | Runtime | TypeScript with Vite, running locally | your employment history is private and there is no reason for it to need a server | | Source | One structured record, several outputs | the record is the asset and every document is derived from it | | Layout | Computed from content, not positioned by hand | a resume reflows when you add a role, and hand positioning breaks the moment it does | | Output | PDF with real text, generated from the same source for both versions | an image of text parses as nothing at all | | Verification | Extract text from your own output and check the order | the only way to know whether the document parses is to parse it | ## 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 build and the PDF generation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A PDF text extraction tool** · free - Why: Phase 2's check is the whole point and it cannot be done by looking at the page. You need to see the text in the order a machine reads it. - Get it: poppler utilities provide a text extractor, or use any library that reports extraction order. - Verify: Extracting a two column PDF visibly interleaves its columns - [ ] **An honest account of your own experience** · free, and it is the most valuable hour here - Why: Everything is derived from it, and it cannot be generated. Roles, dates, projects and metrics you could defend. - Get it: Set aside an hour and write it from memory and records rather than from an existing resume. - Verify: Every role you would mention has an entry with real dates - [ ] **Three real job descriptions** · free - Why: Tailoring and relevance ranking cannot be judged against invented postings, and real ones contain the vague requirements that break naive matching. - Get it: Save three for roles you would actually apply for. - Verify: Three descriptions are stored as fixtures - [ ] **A model API key, or a local model** (optional) · pay per token; tailoring one document is a fraction of a cent - Why: Phase 4 only. The record, the templates and the extraction checks all work without one. - Get it: A provider key with a spend limit, or a local model if you would rather your employment history stayed on the machine. - Verify: A tailored document generates and passes validation ## Quick start ```sh pdftotext -layout plain.pdf - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. - template design, content guidance, scoring, document rendering, and support - proprietary recruiter data - job-board distribution - human coaching - automated application networks If one of those is essential to you, that is the reason to keep paying for Enhancv, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Enhancv 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 resume builder producing documents that look designed and still parse correctly to replace Enhancv. 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 There is a tension at the centre of this and pretending otherwise produces a bad tool. A heavily designed resume, with columns, sidebars, icons and text in graphics, reads well to a person and parses badly to the software that reads it first. A plain single-column document parses reliably and looks unremarkable. So this produces both from one source: a machine-readable version whose text extracts in the right order, and a designed version for a human. It tells you which to send where, and it never lets you produce only the pretty one by accident. The content rule from the start: every claim traces to your own record of what you did. A resume that overstates is a conversation you cannot win in an interview. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running locally · your employment history is private and there is no reason for it to need a server - Source: One structured record, several outputs · the record is the asset and every document is derived from it - Layout: Computed from content, not positioned by hand · a resume reflows when you add a role, and hand positioning breaks the moment it does - Output: PDF with real text, generated from the same source for both versions · an image of text parses as nothing at all - Verification: Extract text from your own output and check the order · the only way to know whether the document parses is to parse it ### Data model (create this before Phase 1) - profile(id, name, contact_json, summary) - entries(id, kind, title, organisation, started_on, ended_on, location, bullets_json, metrics_json) · kind is role, project, education, certification - skills(id, name, category, evidence_entry_ids_json) · a skill with no supporting entry is a claim with no basis - documents(id, name, template_key, target_role, entry_ids_json, generated_at, pdf_path) - generated_lines(document_id, line, source_entry_id, source_kind) · every line traces to an entry - extraction_checks(document_id, at, extracted_text, order_ok, notes) - Nothing appears on a document that does not reference an entry in the record. ### Phase 1 · The record, written once Build: Structured entries for roles, projects, education and certifications, with real dates and any metrics you can support. Skills referencing the entries that evidence them. Write it from what you did rather than from an existing resume. Trap: Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. Done when: Every role and project you would mention has an entry with real dates. A skill with no supporting entry is flagged rather than accepted. Metrics are real figures or empty, never estimates. The record exports as readable Markdown. Do not build yet: No documents, layout or tailoring. ### Phase 2 · A document that parses correctly Build: A single column template with real text, standard section headings and no text inside graphics. Generate a PDF, then extract its text with a tool and confirm the order and the section headings survive. Trap: Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. Trap: Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. Done when: Extracted text from the plain version comes out in reading order, verified with an extraction tool rather than by eye. Section headings appear as text. Contact details are real text, not graphics. Dates extract in a consistent, parseable form. Do not build yet: No designed version or tailoring yet. ### Phase 3 · The designed version, and saying which is which Build: A designed template with columns and visual hierarchy, generated from the same record. Run the same extraction check and report honestly that it parses worse. Label each output with what it is for. Trap: Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. Done when: Both versions generate from one record. The extraction check runs on both and reports the difference. Each output is labelled with its intended use. The plain version is the default for upload, and choosing the designed one for upload produces a warning. Do not build yet: No tailoring yet. ### Phase 4 · Tailoring that cannot invent Build: Given a job description, select and order the entries that fit and rephrase bullets within the meaning of the original. Record the source entry for every generated line. Validate that each line maps to an entry, and reject the document if one does not. Trap: A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. Done when: A tailored document shows the source entry for every line. A crafted document containing an unsupported line fails validation, tested by feeding one to the validator directly. Reordering by relevance to a description changes which entries appear first. Nothing appears that is not in the record. Do not build yet: No length management or export yet. ### Phase 5 · Length, which is a decision rather than a font size Build: Report when content exceeds the target page count and show what could be removed, ranked by relevance to the target role. Enforce a minimum type size below which the document reports overflow rather than shrinking further. Trap: Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. Done when: Content exceeding the page target produces a warning naming the overflow rather than shrinking below the floor. Suggested cuts are ranked by relevance to the target role. Removing a suggested entry brings the document within the target. Type size never falls below the configured floor. Do not build yet: Export and application tracking come last. ### Phase 6 · Versions, records and what you sent where Build: Keep every generated document with its record snapshot and target role. Export the whole thing. Then verify the plain version against a real parsing check by uploading it somewhere that reports what it extracted. Trap: Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. Done when: Every generated document is kept with its record snapshot and target. Searching by company returns the document sent. Export produces all documents and the record. The plain version has been checked against a real parser and the result recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Template design at the level of the commercial product, which is professional design work rather than code. - Content guidance and scoring against what recruiters respond to, which requires data you do not have. - Any claim that a resume will pass a particular system, since systems differ and change and none of them publish their rules. - Distribution to job boards, which is an integration per board. ### README must contain - That designed layouts parse worse than plain ones, and that both are generated - That the plain version is the default for anything uploaded - That every line on a tailored document traces to an entry in your record - That overflow is reported rather than fixed by shrinking type - That every generated document is kept with the record as it was at the time ===== AGENTS.md ===== # Agent instructions · Enhancv indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running locally, One structured record, several outputs, Computed from content, not positioned by hand, PDF with real text, generated from the same source for both versions, Extract text from your own output and check the order. 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 - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ===== BUILD_PLAN.md ===== # Build plan · Enhancv One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The record, written once Everything you might claim exists in one place, in your own words. ### Steps 1. Model entries with dates and bullets Real dates, your own wording, metrics only where you can support them. Files: `src/record.ts` 2. Require evidence for skills A skill references the entries that demonstrate it. Unsupported skills are flagged. Files: `src/skills.ts` 3. Export the record Readable Markdown, so it outlives this tool. Files: `src/export.ts` ### Done when - [ ] Every role and project has an entry with real dates - [ ] An unsupported skill is flagged - [ ] Metrics are real or empty - [ ] The record exports as readable Markdown ### Watch out - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. ## Phase 2 · A document that parses correctly The plain version's extracted text comes out in reading order. ### Steps 1. Build the plain template single column Standard headings, real text, no graphics carrying information. Files: `src/templates/plain.ts` 2. Extract and check the order With a text extraction tool, as an automated check rather than a manual one. Files: `src/verify-extraction.ts` ```sh pdftotext -layout plain.pdf - ``` 3. Make the check part of generation Generating a document runs the extraction check and reports the result. Files: `src/generate.ts` ### Done when - [ ] Extracted text is in reading order - [ ] Section headings appear as text - [ ] Contact details are text rather than graphics - [ ] The extraction check runs as part of generation ### Watch out - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. ## Phase 3 · The designed version, and saying which is which You get a document that looks good, and you are told where to send each version. ### Steps 1. Build the designed template From the same record. Columns and hierarchy, accepting the extraction cost. Files: `src/templates/designed.ts` 2. Report both extraction results Side by side. The comparison is the information the user needs. Files: `src/verify-extraction.ts` 3. Default to plain for upload With a warning if the designed one is chosen for a system that will parse it. Files: `src/generate.ts` ### Done when - [ ] Both versions generate from one record - [ ] Extraction results are reported for both - [ ] Each output is labelled with its use - [ ] Choosing the designed version for upload warns ### Watch out - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. ## Phase 4 · Tailoring that cannot invent A version targeted at a role rearranges what you did and adds nothing. ### Steps 1. Select and order by relevance Against the description. Selection is most of tailoring and needs no model. Files: `src/tailor.ts` 2. Rephrase within meaning only Constrained to the bullet's own content. Ask for structured output naming the source entry. Files: `src/rephrase.ts` 3. Validate every line A pure function, unit tested with a crafted unsupported line. Files: `src/validate.ts`, `src/validate.test.ts` ### Done when - [ ] Every line shows its source entry - [ ] A crafted unsupported line is rejected - [ ] Relevance ordering changes with the description - [ ] Nothing appears outside the record ### Watch out - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. ## Phase 5 · Length, which is a decision rather than a font size Fitting a page means cutting something, not shrinking the type. ### Steps 1. Set a readable floor Below it the document reports overflow. This is the rule that keeps output readable. Files: `src/layout.ts` 2. Rank suggested cuts Least relevant to the target role first. The suggestion is the useful part. Files: `src/trim.ts` 3. Verify by cutting Removing a suggestion should actually bring it within the target. ### Done when - [ ] Overflow warns rather than shrinking below the floor - [ ] Suggested cuts are ranked by relevance - [ ] Removing a suggestion brings it within target - [ ] Type size never falls below the floor ### Watch out - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ## Not in this build - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. ## After v1, if you want it - A cover letter generated from the same record with the same validation - A version comparison showing what changed between two tailored documents - A plain text output for forms that ask you to paste rather than upload ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your experience record. Private, and worth backing up. RECORD_PATH=./data/record.json # Required. Generated PDFs with their record snapshots. DOCUMENTS_DIR=./data/documents # Required. Below this the layout reports overflow instead of shrinking. MIN_FONT_PT=9.5 # Required. Page target. Exceeding it suggests cuts rather than shrinking type. TARGET_PAGES=2 # Required. Which adapter tailoring uses. Local means your history stays on the machine. MODEL_PROVIDER=local
You are building a production product version of Enhancv. 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 ===== # Enhancv · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Enhancv, create visually polished resume variants from structured user evidence. The hard boundary is template design, content guidance, scoring, document rendering, and support, plus data, distribution, and coaching. ## Product outcome One record producing a document that parses correctly and one that looks designed, tailored versions whose every line you can point to, and a history of what you sent where. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local browser - resume and job-description files - optional OpenAI API key ## Explicit non-goals for v1 - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. - template design, content guidance, scoring, document rendering, and support - proprietary recruiter data - job-board distribution - human coaching - automated application networks ## Success criteria - The plain version's extracted text is confirmed in reading order - Both versions' extraction results are reported - A crafted unsupported line is rejected by the validator - Overflow warns rather than shrinking below the floor - Documents are confirmed to keep their record snapshot ===== BRIEF.md ===== # Build brief · Enhancv 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 resume builder producing documents that look designed and still parse correctly to replace Enhancv. 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 There is a tension at the centre of this and pretending otherwise produces a bad tool. A heavily designed resume, with columns, sidebars, icons and text in graphics, reads well to a person and parses badly to the software that reads it first. A plain single-column document parses reliably and looks unremarkable. So this produces both from one source: a machine-readable version whose text extracts in the right order, and a designed version for a human. It tells you which to send where, and it never lets you produce only the pretty one by accident. The content rule from the start: every claim traces to your own record of what you did. A resume that overstates is a conversation you cannot win in an interview. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running locally · your employment history is private and there is no reason for it to need a server - Source: One structured record, several outputs · the record is the asset and every document is derived from it - Layout: Computed from content, not positioned by hand · a resume reflows when you add a role, and hand positioning breaks the moment it does - Output: PDF with real text, generated from the same source for both versions · an image of text parses as nothing at all - Verification: Extract text from your own output and check the order · the only way to know whether the document parses is to parse it ### Data model (create this before Phase 1) - profile(id, name, contact_json, summary) - entries(id, kind, title, organisation, started_on, ended_on, location, bullets_json, metrics_json) · kind is role, project, education, certification - skills(id, name, category, evidence_entry_ids_json) · a skill with no supporting entry is a claim with no basis - documents(id, name, template_key, target_role, entry_ids_json, generated_at, pdf_path) - generated_lines(document_id, line, source_entry_id, source_kind) · every line traces to an entry - extraction_checks(document_id, at, extracted_text, order_ok, notes) - Nothing appears on a document that does not reference an entry in the record. ### Phase 1 · The record, written once Build: Structured entries for roles, projects, education and certifications, with real dates and any metrics you can support. Skills referencing the entries that evidence them. Write it from what you did rather than from an existing resume. Trap: Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. Done when: Every role and project you would mention has an entry with real dates. A skill with no supporting entry is flagged rather than accepted. Metrics are real figures or empty, never estimates. The record exports as readable Markdown. Do not build yet: No documents, layout or tailoring. ### Phase 2 · A document that parses correctly Build: A single column template with real text, standard section headings and no text inside graphics. Generate a PDF, then extract its text with a tool and confirm the order and the section headings survive. Trap: Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. Trap: Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. Done when: Extracted text from the plain version comes out in reading order, verified with an extraction tool rather than by eye. Section headings appear as text. Contact details are real text, not graphics. Dates extract in a consistent, parseable form. Do not build yet: No designed version or tailoring yet. ### Phase 3 · The designed version, and saying which is which Build: A designed template with columns and visual hierarchy, generated from the same record. Run the same extraction check and report honestly that it parses worse. Label each output with what it is for. Trap: Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. Done when: Both versions generate from one record. The extraction check runs on both and reports the difference. Each output is labelled with its intended use. The plain version is the default for upload, and choosing the designed one for upload produces a warning. Do not build yet: No tailoring yet. ### Phase 4 · Tailoring that cannot invent Build: Given a job description, select and order the entries that fit and rephrase bullets within the meaning of the original. Record the source entry for every generated line. Validate that each line maps to an entry, and reject the document if one does not. Trap: A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. Done when: A tailored document shows the source entry for every line. A crafted document containing an unsupported line fails validation, tested by feeding one to the validator directly. Reordering by relevance to a description changes which entries appear first. Nothing appears that is not in the record. Do not build yet: No length management or export yet. ### Phase 5 · Length, which is a decision rather than a font size Build: Report when content exceeds the target page count and show what could be removed, ranked by relevance to the target role. Enforce a minimum type size below which the document reports overflow rather than shrinking further. Trap: Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. Done when: Content exceeding the page target produces a warning naming the overflow rather than shrinking below the floor. Suggested cuts are ranked by relevance to the target role. Removing a suggested entry brings the document within the target. Type size never falls below the configured floor. Do not build yet: Export and application tracking come last. ### Phase 6 · Versions, records and what you sent where Build: Keep every generated document with its record snapshot and target role. Export the whole thing. Then verify the plain version against a real parsing check by uploading it somewhere that reports what it extracted. Trap: Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. Done when: Every generated document is kept with its record snapshot and target. Searching by company returns the document sent. Export produces all documents and the record. The plain version has been checked against a real parser and the result recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Template design at the level of the commercial product, which is professional design work rather than code. - Content guidance and scoring against what recruiters respond to, which requires data you do not have. - Any claim that a resume will pass a particular system, since systems differ and change and none of them publish their rules. - Distribution to job boards, which is an integration per board. ### README must contain - That designed layouts parse worse than plain ones, and that both are generated - That the plain version is the default for anything uploaded - That every line on a tailored document traces to an entry in your record - That overflow is reported rather than fixed by shrinking type - That every generated document is kept with the record as it was at the time ===== ARCHITECTURE.md ===== # Architecture · Enhancv ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running locally | your employment history is private and there is no reason for it to need a server | | Source | One structured record, several outputs | the record is the asset and every document is derived from it | | Layout | Computed from content, not positioned by hand | a resume reflows when you add a role, and hand positioning breaks the moment it does | | Output | PDF with real text, generated from the same source for both versions | an image of text parses as nothing at all | | Verification | Extract text from your own output and check the order | the only way to know whether the document parses is to parse it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Record | Your experience, in your words. | This is the durable asset and should outlive the tool, which is what the Markdown export is for. | | Templates | Plain and designed layouts from one source. | Any renderer. Generating both, and defaulting to plain for upload, is the product decision. | | Extraction check | Knowing how a document parses. | Any extractor. Running it as part of generation is what makes it useful rather than optional. | | Tailoring | Selection, ordering and constrained rephrasing. | Any model. The validator is what makes swapping safe. | | Documents | What was sent, with its record snapshot. | Simple, and it is what you need before an interview. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RECORD_PATH` · required · Your experience record. Private, and worth backing up. - `DOCUMENTS_DIR` · required · Generated PDFs with their record snapshots. - `MIN_FONT_PT` · required · Below this the layout reports overflow instead of shrinking. - `TARGET_PAGES` · required · Page target. Exceeding it suggests cuts rather than shrinking type. - `MODEL_PROVIDER` · required · Which adapter tailoring uses. Local means your history stays on 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 · Enhancv product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, running locally, One structured record, several outputs, Computed from content, not positioned by hand, PDF with real text, generated from the same source for both versions, Extract text from your own output and check the order. - 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 - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. - Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. ===== MILESTONES.md ===== # Delivery milestones · Enhancv Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · The record, written once Everything you might claim exists in one place, in your own words. ### Steps 1. Model entries with dates and bullets Real dates, your own wording, metrics only where you can support them. Files: `src/record.ts` 2. Require evidence for skills A skill references the entries that demonstrate it. Unsupported skills are flagged. Files: `src/skills.ts` 3. Export the record Readable Markdown, so it outlives this tool. Files: `src/export.ts` ### Done when - [ ] Every role and project has an entry with real dates - [ ] An unsupported skill is flagged - [ ] Metrics are real or empty - [ ] The record exports as readable Markdown ### Watch out - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. ## M2 · A document that parses correctly The plain version's extracted text comes out in reading order. ### Steps 1. Build the plain template single column Standard headings, real text, no graphics carrying information. Files: `src/templates/plain.ts` 2. Extract and check the order With a text extraction tool, as an automated check rather than a manual one. Files: `src/verify-extraction.ts` ```sh pdftotext -layout plain.pdf - ``` 3. Make the check part of generation Generating a document runs the extraction check and reports the result. Files: `src/generate.ts` ### Done when - [ ] Extracted text is in reading order - [ ] Section headings appear as text - [ ] Contact details are text rather than graphics - [ ] The extraction check runs as part of generation ### Watch out - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. ## M3 · The designed version, and saying which is which You get a document that looks good, and you are told where to send each version. ### Steps 1. Build the designed template From the same record. Columns and hierarchy, accepting the extraction cost. Files: `src/templates/designed.ts` 2. Report both extraction results Side by side. The comparison is the information the user needs. Files: `src/verify-extraction.ts` 3. Default to plain for upload With a warning if the designed one is chosen for a system that will parse it. Files: `src/generate.ts` ### Done when - [ ] Both versions generate from one record - [ ] Extraction results are reported for both - [ ] Each output is labelled with its use - [ ] Choosing the designed version for upload warns ### Watch out - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. ## M4 · Tailoring that cannot invent A version targeted at a role rearranges what you did and adds nothing. ### Steps 1. Select and order by relevance Against the description. Selection is most of tailoring and needs no model. Files: `src/tailor.ts` 2. Rephrase within meaning only Constrained to the bullet's own content. Ask for structured output naming the source entry. Files: `src/rephrase.ts` 3. Validate every line A pure function, unit tested with a crafted unsupported line. Files: `src/validate.ts`, `src/validate.test.ts` ### Done when - [ ] Every line shows its source entry - [ ] A crafted unsupported line is rejected - [ ] Relevance ordering changes with the description - [ ] Nothing appears outside the record ### Watch out - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. ## M5 · Length, which is a decision rather than a font size Fitting a page means cutting something, not shrinking the type. ### Steps 1. Set a readable floor Below it the document reports overflow. This is the rule that keeps output readable. Files: `src/layout.ts` 2. Rank suggested cuts Least relevant to the target role first. The suggestion is the useful part. Files: `src/trim.ts` 3. Verify by cutting Removing a suggestion should actually bring it within the target. ### Done when - [ ] Overflow warns rather than shrinking below the floor - [ ] Suggested cuts are ranked by relevance - [ ] Removing a suggestion brings it within target - [ ] Type size never falls below the floor ### Watch out - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ## M6 · Versions, records and what you sent where (production only) You can answer which document went to which application, months later. ### Steps 1. Snapshot the record per document So a document generated in March reflects what you claimed in March. Files: `src/documents.ts` 2. Make documents findable by target By company and role. This is what you need before an interview. Files: `src/search.ts` 3. Verify against a real parser Upload the plain version somewhere that reports its extraction and record the outcome. Files: `docs/parsing-check.md` ### Done when - [ ] Documents are kept with their record snapshot - [ ] Searching by company returns the document sent - [ ] Export produces documents and the record - [ ] A real parsing check result is recorded ### Watch out - Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. ===== OPERATIONS.md ===== # Operations · Enhancv ## Backup The record and the generated documents. Both are small and the record in particular is an hour of careful work you would not enjoy repeating. ## Restore Restore both and regenerate a document, comparing against the stored one. Because documents carry their record snapshot, regeneration should reproduce them exactly. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. Worth tracking is the validation rejection rate during tailoring, since a rising rate means the model is filling gaps the record does not cover. ## Incident checklist The failure that matters is sending a document containing a claim you cannot support. The validator is the control, and if one gets through, the fix belongs in the validator. 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 - [ ] The plain version's extracted text is confirmed in reading order - [ ] Both versions' extraction results are reported - [ ] A crafted unsupported line is rejected by the validator - [ ] Overflow warns rather than shrinking below the floor - [ ] Documents are confirmed to keep their record snapshot ## Launch constraint Do not market omitted Enhancv 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 experience record. Private, and worth backing up. RECORD_PATH=./data/record.json # Required. Generated PDFs with their record snapshots. DOCUMENTS_DIR=./data/documents # Required. Below this the layout reports overflow instead of shrinking. MIN_FONT_PT=9.5 # Required. Page target. Exceeding it suggests cuts rather than shrinking type. TARGET_PAGES=2 # Required. Which adapter tailoring uses. Local means your history stays on the machine. MODEL_PROVIDER=local
# Enhancv · indie build One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. 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 | | --- | --- | --- | | Runtime | TypeScript with Vite, running locally | your employment history is private and there is no reason for it to need a server | | Source | One structured record, several outputs | the record is the asset and every document is derived from it | | Layout | Computed from content, not positioned by hand | a resume reflows when you add a role, and hand positioning breaks the moment it does | | Output | PDF with real text, generated from the same source for both versions | an image of text parses as nothing at all | | Verification | Extract text from your own output and check the order | the only way to know whether the document parses is to parse it | ## 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 build and the PDF generation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A PDF text extraction tool** · free - Why: Phase 2's check is the whole point and it cannot be done by looking at the page. You need to see the text in the order a machine reads it. - Get it: poppler utilities provide a text extractor, or use any library that reports extraction order. - Verify: Extracting a two column PDF visibly interleaves its columns - [ ] **An honest account of your own experience** · free, and it is the most valuable hour here - Why: Everything is derived from it, and it cannot be generated. Roles, dates, projects and metrics you could defend. - Get it: Set aside an hour and write it from memory and records rather than from an existing resume. - Verify: Every role you would mention has an entry with real dates - [ ] **Three real job descriptions** · free - Why: Tailoring and relevance ranking cannot be judged against invented postings, and real ones contain the vague requirements that break naive matching. - Get it: Save three for roles you would actually apply for. - Verify: Three descriptions are stored as fixtures - [ ] **A model API key, or a local model** (optional) · pay per token; tailoring one document is a fraction of a cent - Why: Phase 4 only. The record, the templates and the extraction checks all work without one. - Get it: A provider key with a spend limit, or a local model if you would rather your employment history stayed on the machine. - Verify: A tailored document generates and passes validation ## Quick start ```sh pdftotext -layout plain.pdf - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. - template design, content guidance, scoring, document rendering, and support - proprietary recruiter data - job-board distribution - human coaching - automated application networks If one of those is essential to you, that is the reason to keep paying for Enhancv, and the README should say so rather than pretend.
# Build brief · Enhancv 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 resume builder producing documents that look designed and still parse correctly to replace Enhancv. 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 There is a tension at the centre of this and pretending otherwise produces a bad tool. A heavily designed resume, with columns, sidebars, icons and text in graphics, reads well to a person and parses badly to the software that reads it first. A plain single-column document parses reliably and looks unremarkable. So this produces both from one source: a machine-readable version whose text extracts in the right order, and a designed version for a human. It tells you which to send where, and it never lets you produce only the pretty one by accident. The content rule from the start: every claim traces to your own record of what you did. A resume that overstates is a conversation you cannot win in an interview. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running locally · your employment history is private and there is no reason for it to need a server - Source: One structured record, several outputs · the record is the asset and every document is derived from it - Layout: Computed from content, not positioned by hand · a resume reflows when you add a role, and hand positioning breaks the moment it does - Output: PDF with real text, generated from the same source for both versions · an image of text parses as nothing at all - Verification: Extract text from your own output and check the order · the only way to know whether the document parses is to parse it ### Data model (create this before Phase 1) - profile(id, name, contact_json, summary) - entries(id, kind, title, organisation, started_on, ended_on, location, bullets_json, metrics_json) · kind is role, project, education, certification - skills(id, name, category, evidence_entry_ids_json) · a skill with no supporting entry is a claim with no basis - documents(id, name, template_key, target_role, entry_ids_json, generated_at, pdf_path) - generated_lines(document_id, line, source_entry_id, source_kind) · every line traces to an entry - extraction_checks(document_id, at, extracted_text, order_ok, notes) - Nothing appears on a document that does not reference an entry in the record. ### Phase 1 · The record, written once Build: Structured entries for roles, projects, education and certifications, with real dates and any metrics you can support. Skills referencing the entries that evidence them. Write it from what you did rather than from an existing resume. Trap: Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. Done when: Every role and project you would mention has an entry with real dates. A skill with no supporting entry is flagged rather than accepted. Metrics are real figures or empty, never estimates. The record exports as readable Markdown. Do not build yet: No documents, layout or tailoring. ### Phase 2 · A document that parses correctly Build: A single column template with real text, standard section headings and no text inside graphics. Generate a PDF, then extract its text with a tool and confirm the order and the section headings survive. Trap: Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. Trap: Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. Done when: Extracted text from the plain version comes out in reading order, verified with an extraction tool rather than by eye. Section headings appear as text. Contact details are real text, not graphics. Dates extract in a consistent, parseable form. Do not build yet: No designed version or tailoring yet. ### Phase 3 · The designed version, and saying which is which Build: A designed template with columns and visual hierarchy, generated from the same record. Run the same extraction check and report honestly that it parses worse. Label each output with what it is for. Trap: Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. Done when: Both versions generate from one record. The extraction check runs on both and reports the difference. Each output is labelled with its intended use. The plain version is the default for upload, and choosing the designed one for upload produces a warning. Do not build yet: No tailoring yet. ### Phase 4 · Tailoring that cannot invent Build: Given a job description, select and order the entries that fit and rephrase bullets within the meaning of the original. Record the source entry for every generated line. Validate that each line maps to an entry, and reject the document if one does not. Trap: A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. Done when: A tailored document shows the source entry for every line. A crafted document containing an unsupported line fails validation, tested by feeding one to the validator directly. Reordering by relevance to a description changes which entries appear first. Nothing appears that is not in the record. Do not build yet: No length management or export yet. ### Phase 5 · Length, which is a decision rather than a font size Build: Report when content exceeds the target page count and show what could be removed, ranked by relevance to the target role. Enforce a minimum type size below which the document reports overflow rather than shrinking further. Trap: Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. Done when: Content exceeding the page target produces a warning naming the overflow rather than shrinking below the floor. Suggested cuts are ranked by relevance to the target role. Removing a suggested entry brings the document within the target. Type size never falls below the configured floor. Do not build yet: Export and application tracking come last. ### Phase 6 · Versions, records and what you sent where Build: Keep every generated document with its record snapshot and target role. Export the whole thing. Then verify the plain version against a real parsing check by uploading it somewhere that reports what it extracted. Trap: Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. Done when: Every generated document is kept with its record snapshot and target. Searching by company returns the document sent. Export produces all documents and the record. The plain version has been checked against a real parser and the result recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Template design at the level of the commercial product, which is professional design work rather than code. - Content guidance and scoring against what recruiters respond to, which requires data you do not have. - Any claim that a resume will pass a particular system, since systems differ and change and none of them publish their rules. - Distribution to job boards, which is an integration per board. ### README must contain - That designed layouts parse worse than plain ones, and that both are generated - That the plain version is the default for anything uploaded - That every line on a tailored document traces to an entry in your record - That overflow is reported rather than fixed by shrinking type - That every generated document is kept with the record as it was at the time
# Agent instructions · Enhancv indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running locally, One structured record, several outputs, Computed from content, not positioned by hand, PDF with real text, generated from the same source for both versions, Extract text from your own output and check the order. 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 - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout.
# Build plan · Enhancv One structured record of your experience producing several tailored resumes: a plain version whose extracted text comes out in the right order, a designed version for a human reader, tailoring that only rearranges and rephrases what you recorded, and a check that every generated line traces back to an entry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · The record, written once Everything you might claim exists in one place, in your own words. ### Steps 1. Model entries with dates and bullets Real dates, your own wording, metrics only where you can support them. Files: `src/record.ts` 2. Require evidence for skills A skill references the entries that demonstrate it. Unsupported skills are flagged. Files: `src/skills.ts` 3. Export the record Readable Markdown, so it outlives this tool. Files: `src/export.ts` ### Done when - [ ] Every role and project has an entry with real dates - [ ] An unsupported skill is flagged - [ ] Metrics are real or empty - [ ] The record exports as readable Markdown ### Watch out - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. ## Phase 2 · A document that parses correctly The plain version's extracted text comes out in reading order. ### Steps 1. Build the plain template single column Standard headings, real text, no graphics carrying information. Files: `src/templates/plain.ts` 2. Extract and check the order With a text extraction tool, as an automated check rather than a manual one. Files: `src/verify-extraction.ts` ```sh pdftotext -layout plain.pdf - ``` 3. Make the check part of generation Generating a document runs the extraction check and reports the result. Files: `src/generate.ts` ### Done when - [ ] Extracted text is in reading order - [ ] Section headings appear as text - [ ] Contact details are text rather than graphics - [ ] The extraction check runs as part of generation ### Watch out - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. ## Phase 3 · The designed version, and saying which is which You get a document that looks good, and you are told where to send each version. ### Steps 1. Build the designed template From the same record. Columns and hierarchy, accepting the extraction cost. Files: `src/templates/designed.ts` 2. Report both extraction results Side by side. The comparison is the information the user needs. Files: `src/verify-extraction.ts` 3. Default to plain for upload With a warning if the designed one is chosen for a system that will parse it. Files: `src/generate.ts` ### Done when - [ ] Both versions generate from one record - [ ] Extraction results are reported for both - [ ] Each output is labelled with its use - [ ] Choosing the designed version for upload warns ### Watch out - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. ## Phase 4 · Tailoring that cannot invent A version targeted at a role rearranges what you did and adds nothing. ### Steps 1. Select and order by relevance Against the description. Selection is most of tailoring and needs no model. Files: `src/tailor.ts` 2. Rephrase within meaning only Constrained to the bullet's own content. Ask for structured output naming the source entry. Files: `src/rephrase.ts` 3. Validate every line A pure function, unit tested with a crafted unsupported line. Files: `src/validate.ts`, `src/validate.test.ts` ### Done when - [ ] Every line shows its source entry - [ ] A crafted unsupported line is rejected - [ ] Relevance ordering changes with the description - [ ] Nothing appears outside the record ### Watch out - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. ## Phase 5 · Length, which is a decision rather than a font size Fitting a page means cutting something, not shrinking the type. ### Steps 1. Set a readable floor Below it the document reports overflow. This is the rule that keeps output readable. Files: `src/layout.ts` 2. Rank suggested cuts Least relevant to the target role first. The suggestion is the useful part. Files: `src/trim.ts` 3. Verify by cutting Removing a suggestion should actually bring it within the target. ### Done when - [ ] Overflow warns rather than shrinking below the floor - [ ] Suggested cuts are ranked by relevance - [ ] Removing a suggestion brings it within target - [ ] Type size never falls below the floor ### Watch out - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ## Not in this build - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. ## After v1, if you want it - A cover letter generated from the same record with the same validation - A version comparison showing what changed between two tailored documents - A plain text output for forms that ask you to paste rather than upload
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your experience record. Private, and worth backing up. RECORD_PATH=./data/record.json # Required. Generated PDFs with their record snapshots. DOCUMENTS_DIR=./data/documents # Required. Below this the layout reports overflow instead of shrinking. MIN_FONT_PT=9.5 # Required. Page target. Exceeding it suggests cuts rather than shrinking type. TARGET_PAGES=2 # Required. Which adapter tailoring uses. Local means your history stays on the machine. MODEL_PROVIDER=local
# Enhancv · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Enhancv, create visually polished resume variants from structured user evidence. The hard boundary is template design, content guidance, scoring, document rendering, and support, plus data, distribution, and coaching. ## Product outcome One record producing a document that parses correctly and one that looks designed, tailored versions whose every line you can point to, and a history of what you sent where. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local browser - resume and job-description files - optional OpenAI API key ## Explicit non-goals for v1 - Professional template design. - Content guidance based on recruiter data. - Any guarantee about a particular parsing system. - Job board distribution. - template design, content guidance, scoring, document rendering, and support - proprietary recruiter data - job-board distribution - human coaching - automated application networks ## Success criteria - The plain version's extracted text is confirmed in reading order - Both versions' extraction results are reported - A crafted unsupported line is rejected by the validator - Overflow warns rather than shrinking below the floor - Documents are confirmed to keep their record snapshot
# Build brief · Enhancv 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 resume builder producing documents that look designed and still parse correctly to replace Enhancv. 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 There is a tension at the centre of this and pretending otherwise produces a bad tool. A heavily designed resume, with columns, sidebars, icons and text in graphics, reads well to a person and parses badly to the software that reads it first. A plain single-column document parses reliably and looks unremarkable. So this produces both from one source: a machine-readable version whose text extracts in the right order, and a designed version for a human. It tells you which to send where, and it never lets you produce only the pretty one by accident. The content rule from the start: every claim traces to your own record of what you did. A resume that overstates is a conversation you cannot win in an interview. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running locally · your employment history is private and there is no reason for it to need a server - Source: One structured record, several outputs · the record is the asset and every document is derived from it - Layout: Computed from content, not positioned by hand · a resume reflows when you add a role, and hand positioning breaks the moment it does - Output: PDF with real text, generated from the same source for both versions · an image of text parses as nothing at all - Verification: Extract text from your own output and check the order · the only way to know whether the document parses is to parse it ### Data model (create this before Phase 1) - profile(id, name, contact_json, summary) - entries(id, kind, title, organisation, started_on, ended_on, location, bullets_json, metrics_json) · kind is role, project, education, certification - skills(id, name, category, evidence_entry_ids_json) · a skill with no supporting entry is a claim with no basis - documents(id, name, template_key, target_role, entry_ids_json, generated_at, pdf_path) - generated_lines(document_id, line, source_entry_id, source_kind) · every line traces to an entry - extraction_checks(document_id, at, extracted_text, order_ok, notes) - Nothing appears on a document that does not reference an entry in the record. ### Phase 1 · The record, written once Build: Structured entries for roles, projects, education and certifications, with real dates and any metrics you can support. Skills referencing the entries that evidence them. Write it from what you did rather than from an existing resume. Trap: Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. Done when: Every role and project you would mention has an entry with real dates. A skill with no supporting entry is flagged rather than accepted. Metrics are real figures or empty, never estimates. The record exports as readable Markdown. Do not build yet: No documents, layout or tailoring. ### Phase 2 · A document that parses correctly Build: A single column template with real text, standard section headings and no text inside graphics. Generate a PDF, then extract its text with a tool and confirm the order and the section headings survive. Trap: Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. Trap: Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. Done when: Extracted text from the plain version comes out in reading order, verified with an extraction tool rather than by eye. Section headings appear as text. Contact details are real text, not graphics. Dates extract in a consistent, parseable form. Do not build yet: No designed version or tailoring yet. ### Phase 3 · The designed version, and saying which is which Build: A designed template with columns and visual hierarchy, generated from the same record. Run the same extraction check and report honestly that it parses worse. Label each output with what it is for. Trap: Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. Done when: Both versions generate from one record. The extraction check runs on both and reports the difference. Each output is labelled with its intended use. The plain version is the default for upload, and choosing the designed one for upload produces a warning. Do not build yet: No tailoring yet. ### Phase 4 · Tailoring that cannot invent Build: Given a job description, select and order the entries that fit and rephrase bullets within the meaning of the original. Record the source entry for every generated line. Validate that each line maps to an entry, and reject the document if one does not. Trap: A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. Done when: A tailored document shows the source entry for every line. A crafted document containing an unsupported line fails validation, tested by feeding one to the validator directly. Reordering by relevance to a description changes which entries appear first. Nothing appears that is not in the record. Do not build yet: No length management or export yet. ### Phase 5 · Length, which is a decision rather than a font size Build: Report when content exceeds the target page count and show what could be removed, ranked by relevance to the target role. Enforce a minimum type size below which the document reports overflow rather than shrinking further. Trap: Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. Done when: Content exceeding the page target produces a warning naming the overflow rather than shrinking below the floor. Suggested cuts are ranked by relevance to the target role. Removing a suggested entry brings the document within the target. Type size never falls below the configured floor. Do not build yet: Export and application tracking come last. ### Phase 6 · Versions, records and what you sent where Build: Keep every generated document with its record snapshot and target role. Export the whole thing. Then verify the plain version against a real parsing check by uploading it somewhere that reports what it extracted. Trap: Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time. Done when: Every generated document is kept with its record snapshot and target. Searching by company returns the document sent. Export produces all documents and the record. The plain version has been checked against a real parser and the result recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Template design at the level of the commercial product, which is professional design work rather than code. - Content guidance and scoring against what recruiters respond to, which requires data you do not have. - Any claim that a resume will pass a particular system, since systems differ and change and none of them publish their rules. - Distribution to job boards, which is an integration per board. ### README must contain - That designed layouts parse worse than plain ones, and that both are generated - That the plain version is the default for anything uploaded - That every line on a tailored document traces to an entry in your record - That overflow is reported rather than fixed by shrinking type - That every generated document is kept with the record as it was at the time
# Architecture · Enhancv ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running locally | your employment history is private and there is no reason for it to need a server | | Source | One structured record, several outputs | the record is the asset and every document is derived from it | | Layout | Computed from content, not positioned by hand | a resume reflows when you add a role, and hand positioning breaks the moment it does | | Output | PDF with real text, generated from the same source for both versions | an image of text parses as nothing at all | | Verification | Extract text from your own output and check the order | the only way to know whether the document parses is to parse it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Record | Your experience, in your words. | This is the durable asset and should outlive the tool, which is what the Markdown export is for. | | Templates | Plain and designed layouts from one source. | Any renderer. Generating both, and defaulting to plain for upload, is the product decision. | | Extraction check | Knowing how a document parses. | Any extractor. Running it as part of generation is what makes it useful rather than optional. | | Tailoring | Selection, ordering and constrained rephrasing. | Any model. The validator is what makes swapping safe. | | Documents | What was sent, with its record snapshot. | Simple, and it is what you need before an interview. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RECORD_PATH` · required · Your experience record. Private, and worth backing up. - `DOCUMENTS_DIR` · required · Generated PDFs with their record snapshots. - `MIN_FONT_PT` · required · Below this the layout reports overflow instead of shrinking. - `TARGET_PAGES` · required · Page target. Exceeding it suggests cuts rather than shrinking type. - `MODEL_PROVIDER` · required · Which adapter tailoring uses. Local means your history stays on 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 · Enhancv product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, running locally, One structured record, several outputs, Computed from content, not positioned by hand, PDF with real text, generated from the same source for both versions, Extract text from your own output and check the order. - 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 - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. - Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time.
# Delivery milestones · Enhancv Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · The record, written once Everything you might claim exists in one place, in your own words. ### Steps 1. Model entries with dates and bullets Real dates, your own wording, metrics only where you can support them. Files: `src/record.ts` 2. Require evidence for skills A skill references the entries that demonstrate it. Unsupported skills are flagged. Files: `src/skills.ts` 3. Export the record Readable Markdown, so it outlives this tool. Files: `src/export.ts` ### Done when - [ ] Every role and project has an entry with real dates - [ ] An unsupported skill is flagged - [ ] Metrics are real or empty - [ ] The record exports as readable Markdown ### Watch out - Building from an existing resume means you are rephrasing a rephrasing, and small inflations compound until you cannot remember what you actually claimed where. Write the record from the work itself, and treat a metric you cannot defend in an interview as one that should be empty. ## M2 · A document that parses correctly The plain version's extracted text comes out in reading order. ### Steps 1. Build the plain template single column Standard headings, real text, no graphics carrying information. Files: `src/templates/plain.ts` 2. Extract and check the order With a text extraction tool, as an automated check rather than a manual one. Files: `src/verify-extraction.ts` ```sh pdftotext -layout plain.pdf - ``` 3. Make the check part of generation Generating a document runs the extraction check and reports the result. Files: `src/generate.ts` ### Done when - [ ] Extracted text is in reading order - [ ] Section headings appear as text - [ ] Contact details are text rather than graphics - [ ] The extraction check runs as part of generation ### Watch out - Multi-column layouts extract by position rather than by reading order, so a two column resume commonly comes out interleaved: a line of your job title followed by a line from the sidebar. Verify by extracting rather than by looking at the page. - Icons or text rendered as images extract as nothing, so contact details in a graphic simply do not exist to whatever reads the file first. ## M3 · The designed version, and saying which is which You get a document that looks good, and you are told where to send each version. ### Steps 1. Build the designed template From the same record. Columns and hierarchy, accepting the extraction cost. Files: `src/templates/designed.ts` 2. Report both extraction results Side by side. The comparison is the information the user needs. Files: `src/verify-extraction.ts` 3. Default to plain for upload With a warning if the designed one is chosen for a system that will parse it. Files: `src/generate.ts` ### Done when - [ ] Both versions generate from one record - [ ] Extraction results are reported for both - [ ] Each output is labelled with its use - [ ] Choosing the designed version for upload warns ### Watch out - Producing only the designed version, or defaulting to it, means it gets uploaded to systems that read it badly and the application fails before a person sees it. Generate both, report both extraction results, and make the plain one the default for anything uploaded. ## M4 · Tailoring that cannot invent A version targeted at a role rearranges what you did and adds nothing. ### Steps 1. Select and order by relevance Against the description. Selection is most of tailoring and needs no model. Files: `src/tailor.ts` 2. Rephrase within meaning only Constrained to the bullet's own content. Ask for structured output naming the source entry. Files: `src/rephrase.ts` 3. Validate every line A pure function, unit tested with a crafted unsupported line. Files: `src/validate.ts`, `src/validate.test.ts` ### Done when - [ ] Every line shows its source entry - [ ] A crafted unsupported line is rejected - [ ] Relevance ordering changes with the description - [ ] Nothing appears outside the record ### Watch out - A model asked to tailor a resume will add a plausible achievement, and it will read as the strongest line on the page. Validate every line against the record and reject rather than trusting an instruction, because the failure surfaces in an interview where you have to either defend it or retract it. ## M5 · Length, which is a decision rather than a font size Fitting a page means cutting something, not shrinking the type. ### Steps 1. Set a readable floor Below it the document reports overflow. This is the rule that keeps output readable. Files: `src/layout.ts` 2. Rank suggested cuts Least relevant to the target role first. The suggestion is the useful part. Files: `src/trim.ts` 3. Verify by cutting Removing a suggestion should actually bring it within the target. ### Done when - [ ] Overflow warns rather than shrinking below the floor - [ ] Suggested cuts are ranked by relevance - [ ] Removing a suggestion brings it within target - [ ] Type size never falls below the floor ### Watch out - Auto-shrinking to fit produces a resume at eight point type that nobody reads and that signals you could not edit. Report the overflow, suggest the least relevant entries, and let the person cut, because deciding what to remove is judgement rather than layout. ## M6 · Versions, records and what you sent where (production only) You can answer which document went to which application, months later. ### Steps 1. Snapshot the record per document So a document generated in March reflects what you claimed in March. Files: `src/documents.ts` 2. Make documents findable by target By company and role. This is what you need before an interview. Files: `src/search.ts` 3. Verify against a real parser Upload the plain version somewhere that reports its extraction and record the outcome. Files: `docs/parsing-check.md` ### Done when - [ ] Documents are kept with their record snapshot - [ ] Searching by company returns the document sent - [ ] Export produces documents and the record - [ ] A real parsing check result is recorded ### Watch out - Sending a tailored resume and not keeping it means arriving at an interview unable to remember what you claimed, which is a bad position for reasons that have nothing to do with honesty. Keep every generated document with the record as it was at the time.
# Operations · Enhancv ## Backup The record and the generated documents. Both are small and the record in particular is an hour of careful work you would not enjoy repeating. ## Restore Restore both and regenerate a document, comparing against the stored one. Because documents carry their record snapshot, regeneration should reproduce them exactly. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. Worth tracking is the validation rejection rate during tailoring, since a rising rate means the model is filling gaps the record does not cover. ## Incident checklist The failure that matters is sending a document containing a claim you cannot support. The validator is the control, and if one gets through, the fix belongs in the validator. 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 - [ ] The plain version's extracted text is confirmed in reading order - [ ] Both versions' extraction results are reported - [ ] A crafted unsupported line is rejected by the validator - [ ] Overflow warns rather than shrinking below the floor - [ ] Documents are confirmed to keep their record snapshot ## Launch constraint Do not market omitted Enhancv 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 experience record. Private, and worth backing up. RECORD_PATH=./data/record.json # Required. Generated PDFs with their record snapshots. DOCUMENTS_DIR=./data/documents # Required. Below this the layout reports overflow instead of shrinking. MIN_FONT_PT=9.5 # Required. Page target. Exceeding it suggests cuts rather than shrinking type. TARGET_PAGES=2 # Required. Which adapter tailoring uses. Local means your history stays on the machine. MODEL_PROVIDER=local
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Enhancv because people pay for convenience, curated guidance, and distribution; the personal tracking and drafting loop is highly buildable. The recurring cost buys document parsing, truthful claim handling, job-source changes, browser automation rules, privacy, model drift, and user review, not just the visible interface.
xtemplate design, content guidance, scoring, document rendering, and support
xproprietary recruiter data
xjob-board distribution
xhuman coaching
xautomated application networks
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Enhancv pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free access | $0/user | $0/user | Valid for 7 days; maximum 12 section items; all resume templates but Enhancv branding. |
| pro monthly | $24.99/user | — | Up to 300 resumes and cover letters; unlimited section items; no branding. |
| pro quarterly | $16.66/user | — | Up to 300 resumes and cover letters; unlimited section items; no branding. |
| pro semiannual | $13.32/user | — | Up to 300 resumes and cover letters; unlimited section items; no branding. |
| business | $99/workspace | — | Unlimited users, resumes and cover letters; custom templates and branding; commercial use; starts at $99/month. |
free tier7-day free access with a maximum of 12 section items; not an indefinite free tier
billingPro offers monthly + quarterly + 6-month billing, with no annual plan verified; Business starts at $99/month with custom terms
hidden costsQuarterly and semiannual plans are charged upfront; any discretionary prorated refund can reprice used months at the higher monthly rate; taxes and regional-currency pricing may change the checkout total.
verified 2026-08-14 · source ↗
Vibecode Enhancv
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Enhancv replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Enhancv cost?
Enhancv costs about $24.99/month (Pro, checked 2026-07-31), which is $299.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Enhancv?
Honestly: template design, content guidance, scoring, document rendering, and support; proprietary recruiter data; job-board distribution; human coaching; automated application networks. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Enhancv?
Yes: Reactive Resume (A polished resume studio with enough templates to make tasteful a user setting.) Resume Matcher (Tailors a master resume to a pasted job description, then exports the result without selling you credits.) The prompt is for when you want it exactly your way.