Skip to content

Repository files navigation

CV Maker

A desktop app for building CVs that survive Applicant Tracking System parsers. Several people share the app; each keeps one master profile holding everything they have ever done, and derives as many tailored CVs from it as they need — one per role, field, or language.

Exports to PDF (with a real text layer) and Word .docx, and scores every CV against 25 ATS rules as you type.

+------------+--------------------------+------------------+
| People     |  Section editor          |  Live preview    |
|  omer      |  (forms generated from   |  (the same HTML  |
|  berna     |   the section registry)  |   that becomes   |
|  nazan     |                          |   the PDF)       |
|            |  - add/remove sections   +------------------+
| CVs        |  - reorder them          |  ATS panel       |
|  Embedded  |  - tick which entries    |  score, what to  |
|  General   |    appear on this CV     |  fix, job match  |
+------------+--------------------------+------------------+

Running it

python -m venv .venv && .venv/Scripts/pip install -e ".[dev]"
.venv/Scripts/python -m cvmaker

On macOS or Linux the paths are .venv/bin/ instead of .venv/Scripts/.

How the data model works

The thing that makes tailoring practical is that a CV is a view over a profile, not a copy of one.

  • profile.json holds every job, degree, project, and skill the person has ever had, each with a stable id.
  • Each CV file stores references to those ids, plus its own ordering, its own summary, and any per-CV field overrides.

So fixing a job title once fixes it on all three of your CVs. Unticking a role removes it from this CV while leaving it in your profile. Deleting it from the profile tells you which CVs were using it first.

Where your data lives

In %APPDATA%/CVMaker/ on Windows — outside this repository, so nothing personal can be committed by accident. .gitignore blocks profiles/ as a second line of defence. The only profile in the repo is the anonymised examples/sample_profile.json.

Writes are atomic (temp file, fsync, replace) and the last 10 versions of every file are kept in backups/, so a crash mid-save cannot cost you a profile.

Sections

A new CV opens with the five sections recruiters and parsers expect:

Contact Information · Professional Summary · Work Experience · Education · Skills

You can add any of: Projects · Certifications · Languages · Publications · Volunteering · Awards and Honours · Courses and Training · References.

You can reorder sections, hide them, and rename a header — but only to one of that section's recognised synonyms ("Work Experience" → "Professional Experience" → "Employment History"). Free-form headers are deliberately not allowed: parsers use the header to decide what the content underneath means, so inventing one defeats the point of the app.

What the ATS score does and does not mean

The score is a heuristic built from published ATS vendor guidance and long-standing recruiter advice. It checks 25 rules across five weighted categories:

Category Weight Checks
Contact details 20 email and phone present and well-formed, location, contact block in the document body
Structure 25 core sections present and non-empty, recognised headers, reverse-chronological order, no duplicates
Content quality 25 bullets open with action verbs, at least one measured result per role, bullet length, no first-person pronouns
Formatting 15 consistent MM/YYYY dates, no smart quotes or emoji, no tables or images, length under two pages
Keyword match 15 coverage against a pasted job description

Two behaviours worth knowing:

  • Any error-severity finding caps the score at 69. A CV with no email address can never show green, however good the rest of it is.
  • Rules with nothing to judge are excluded, not passed. An empty CV scores 7, not 69 — it earns no credit for mistakes it is too empty to make.

It is not a simulation of any particular employer's ATS, and no offline tool can be — vendors do not publish their parsers. A high score means nothing in the CV is known to break parsers. It is not a prediction that you will get an interview.

The keyword panel lists terms from a posting that your CV is missing. It will never insert them for you: keyword stuffing is itself an ATS risk, and a CV you cannot defend in an interview is worse than a lower number.

What makes the output ATS-safe

Both templates (Classic and Compact — they differ in spacing only) guarantee:

  • Single column. No tables, text boxes, or floats for content.
  • No images, icons, logos, or photographs.
  • No header or footer regions; everything is in the document body.
  • Calibri / Arial / Georgia at 10–12pt.
  • Standard section headers, plain bullets, MM/YYYY dates throughout.
  • Ligatures disabled. Left on, QtWebEngine renders "firmware" with an fi ligature that text extraction returns as U+FB01, silently breaking keyword matching. This was caught by the test below and is exactly the class of bug the app exists to prevent.

The .docx is generated independently from the same document model, using Word's built-in Heading 1 and List Bullet styles rather than manual formatting, so it carries a real outline for parsers to walk.

The preview

The preview pane renders the actual PDF and shows it as real A4 pages, using Qt's PDF viewer. It is not an HTML approximation: it is the exported file, so it cannot disagree with what you send. Page breaks, margins and the page count are all real, and the pane says how many pages you are on.

Above it, a layout bar controls page density: margin, body size, line height, section gap, entry gap, heading size, name size, and whether a page break may split an entry in half. Reset returns to the template's defaults.

Settings are stored per CV and only the dials you actually move are saved, so switching between Classic and Compact still changes everything you left alone. The same values are applied to the .docx, so the Word file matches the pages you laid out.

There is deliberately no free placement. Dragging blocks to arbitrary positions requires absolute positioning or text boxes, and ATS parsers cannot read either in reading order. An app that exists to produce parser-safe CVs should not ship the one feature guaranteed to break them, so placement is controlled through order and density instead. A test asserts no layout control is positional.

Updates

The app can check GitHub for a newer release: Help -> Check for updates. If one exists it shows the release notes and offers to download the installer and run it, which upgrades in place. Your profiles and CVs are never touched by an update.

The check is opt-in and off by default. It is the only time this app touches the network, and the request carries nothing but a User-Agent - GitHub sees an IP address and the app version. There is a test that walks the module's syntax tree asserting it cannot even reference profile data, so that guarantee cannot rot silently.

Publishing a release is two commands:

python tools/release.py build
python tools/release.py tag --push

The tag triggers .github/workflows/release.yml, which builds the installer on a Windows runner and attaches it to a GitHub release. That release is what the update check reads.

The workflow refuses to publish if the tag and VERSION disagree - a release tagged v1.1.0 containing a 1.0.9 build would offer users an "update" that installs an older app. release.py tag derives the tag from VERSION so the two cannot drift apart by hand.

Set the repository the app checks in cvmaker/core/updates.py (DEFAULT_REPO). Note that the installer is unsigned, so each update will show the SmartScreen warning; the dialog says so rather than letting it come as a surprise.

Building the installer

python tools/release.py build

That bumps the patch version, packages the app with PyInstaller, and compiles a Windows installer with Inno Setup, leaving dist/CVMaker-Setup-<version>.exe.

The installer gives the user a wizard with a choose install location page, a Start Menu entry, an optional desktop shortcut, and a proper uninstaller registered in Add/Remove Programs. It installs per-user by default, so no administrator prompt — an all-users install is offered on the privileges page for anyone who wants it.

Uninstalling never deletes your CVs. The program goes in the folder you chose; your data lives in %APPDATA%\CVMaker, and the uninstaller does not touch it.

Build prerequisites, both one-time:

winget install -e --id JRSoftware.InnoSetup
python -m pip install -e ".[dev]"

If Inno Setup is somewhere unusual, point INNO_SETUP_ISCC at ISCC.exe. To build just the application folder without an installer, use --exe-only.

There is no application icon yet. Drop a .ico at installer/cvmaker.ico and both the executable and the installer pick it up on the next build.

Versioning

VERSION at the repository root is the single source of truth. The package metadata, the window title, Help → About, python -m cvmaker --version, and the installer's Add/Remove Programs entry all read from it.

One rule: a build increments the patch of whatever is currently there.

python tools/release.py version      # what is it now
python tools/release.py set 1.1.0    # take manual control
python tools/release.py bump --minor # 1.0.203 -> 1.1.0, without building

So automatic builds walk 1.0.1, 1.0.2 … 1.0.203. When you decide something big has changed, set 1.1.0. The next automatic build is 1.1.1, and counting continues from there. No special cases, and no separate "release mode" to remember.

Two guarantees worth knowing:

  • A failed build rolls the version back. A version number should mean "an artifact with this label exists", not "a build was attempted".
  • AppId in the installer script never changes. That is what makes installing 1.0.4 over 1.0.3 an upgrade rather than a second entry in Add/Remove Programs.

Tests

.venv/Scripts/python -m pytest

400 tests. cvmaker/core/ never imports Qt — enforced by a test — so all the interesting logic runs headlessly.

The one that matters most is the extraction test: it renders a full CV to PDF, pulls the text back out with pdfminer.six, and asserts every field is present and in display order. That is the closest local proxy for "will an ATS read this", and it is what protects every future template change.

Architecture

cvmaker/
  core/       models, section registry, storage, resolve, i18n, view, ats/, keywords
  render/     html.py (Jinja2), pdf.py (QtWebEngine), docx_writer.py
  ui/         PySide6 panes; thin, holds no CV logic
  templates/  Jinja2 HTML + Classic/Compact CSS

One rule holds it together: core/ never imports Qt. Data flows one way — Profile + CV → ResolvedCV → {HTML, PDF, DOCX, ATS findings} — and the analyzer reads the resolved document, never the widget state, so the score always describes exactly what will be exported.

Design and plan documents live in docs/superpowers/.

Licence

MIT.

About

A desktop application to create ATS compatible CV, analysing the ATS score, job description compatibility and many more features.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages