Module 2 — Resume Engine

Takes a job description and the full profile repository, uses Claude to select and tailor content, validates the response, renders LaTeX, compiles to PDF, and displays a preview.

Contents

Pipeline Overview

Profile (SQLite) + Job Description
        │
        ▼
  Step 1: Prompt Construction (agent.ts)
        │
        ▼
  Step 2: Claude API → structured JSON
        │
        ▼
  Step 3: Zod Validation (validator.ts)
        │  on failure → retry (max 2) → mark parse_failed
        ▼
  Step 4: Nunjucks Rendering (renderer.ts) → .tex file
        │
        ▼
  Step 5: xelatex Compilation (compiler.ts) → PDF
        │  on failure → actionable error surfaced via IPC
        ▼
  Step 6: PDF Preview (previewer.ts) → Electron Chromium renderer

Step 1 — Prompt Construction (agent.ts)

Profile entries are fetched from SQLite and serialized to a structured text block. The Claude prompt is assembled from:

  • Serialized profile entries
  • Raw job description text
  • Target template schema (field names, constraints, max bullets per role)

Total profile size is bounded by profile_entry_word_limit across entries. If the combined payload still approaches the model’s context limit, the job description is truncated to fit — profile content is never dropped, as it is the authoritative source material.


Step 2 — Structured LLM Response

Claude returns a strict JSON object only. No LaTeX is generated at this stage:

{
  "summary": "string",
  "experience": [
    {
      "company": "string",
      "role": "string",
      "start_date": "string",
      "end_date": "string",
      "bullets": ["string"]
    }
  ],
  "skills": {
    "languages": ["string"],
    "frameworks": ["string"],
    "tools": ["string"]
  },
  "education": [
    {
      "institution": "string",
      "degree": "string",
      "year": "string"
    }
  ],
  "credentials": ["string"]
}

Step 3 — Validation (validator.ts)

Zod validates against a versioned schema. Constraints enforced:

  • Required fields present and non-empty
  • Bullet strings within configured character limit
  • Date strings match expected format
  • At least one experience entry

On validation failure, error messages are fed back into a retry call (max 2 retries). After retries are exhausted the posting is marked parse_failed in the UI with the option to retry manually.


Step 4 — Nunjucks Rendering (renderer.ts)

The validated object is passed to the selected .tex.njk template via Nunjucks. Template syntax is nearly identical to Jinja2 — {% for %}, {% if %}, {{ var }} all behave the same way. Output is a .tex file written to <userData>/resumes/<application_id>/resume.tex.

// core/resume/renderer.ts
import nunjucks from 'nunjucks';
import fs from 'fs';
import path from 'path';

const env = nunjucks.configure(path.join(__dirname, '../../templates/resume'), {
  autoescape: false, // LaTeX content must not be HTML-escaped
});

export function renderTex(templateName: string, data: ResumeData, outPath: string): void {
  const tex = env.render(`${templateName}.tex.njk`, data);
  fs.writeFileSync(outPath, tex, 'utf-8');
}

Step 5 — xelatex Compilation (compiler.ts)

child_process.spawn call to xelatex with --no-shell-escape explicitly enforced. stdout/stderr are captured. On failure, the LaTeX error log is parsed for the most actionable line and surfaced to the renderer via IPC.

Recompile from snapshot: If the .tex file is missing (e.g. after reinstall), compiler.ts regenerates it from the JSON snapshot stored in the applications table before compiling. The stored artifact is the .tex file — the PDF is always reconstructable.


Step 6 — PDF Preview (previewer.ts)

Electron’s Chromium renderer displays PDFs natively. The compiled PDF path is sent to the renderer process, which loads it in an <iframe> with src="file://...". No image conversion needed — the PDF renders at full fidelity.


Schema Versioning

Application
  id              TEXT (UUID)
  posting_id      TEXT  FK → job_postings.id
  tex_path        TEXT             -- Relative: resumes/<application_id>/resume.tex
  resume_json     TEXT (JSON)      -- Point-in-time snapshot for recompile-from-snapshot
  schema_version  INTEGER          -- Incremented when Zod schema changes
  applied_at      TEXT (ISO datetime)
  notes           TEXT | NULL

Application is a resume artifact record only. All status tracking lives on job_postings.

When the schema is updated, a migration handles re-serializing or flagging old snapshots. Old resumes that cannot be re-parsed against the current schema are marked legacy-only — the PDF is still accessible via recompile, but the JSON is not re-parseable.


Templates

Two templates ship with the app:

Template File
classic templates/resume/classic.tex.njk
modern templates/resume/modern.tex.njk

The user selects a template before initiating tailoring. The template name is passed to renderer.ts which loads the corresponding .tex.njk file via Nunjucks.


CareerIndex — local-first, all data stays on your machine.

This site uses Just the Docs, a documentation theme for Jekyll.