How CareerOps Handles Generated PDFs and Evaluation Reports: A Technical Deep Dive
CareerOps generates ATS-compliant PDFs from HTML CVs using Playwright, indexes them by report number in a TSV manifest, and synchronizes them with evaluation reports through an automated pipeline.
CareerOps is an open-source job application tracker that automates the creation of professional CVs and their associated evaluation reports. The system treats generated PDFs and evaluation reports as tightly coupled artefacts, ensuring that every evaluation report can automatically reference its corresponding PDF through a persistent manifest-based indexing system.
The PDF Generation Pipeline
The core conversion logic resides in generate-pdf.mjs, which orchestrates HTML normalization, rendering, and manifest registration.
HTML Normalization for ATS Compatibility
Before rendering, CareerOps sanitizes the input HTML to maximize compatibility with Applicant Tracking Systems (ATS). The script replaces Unicode characters that break parser logic—such as em-dashes, smart quotes, and zero-width spaces—with safe ASCII equivalents. This ATS normalization happens in generate-pdf.mjs prior to any PDF generation, ensuring that the final document passes through automated screening systems without character encoding issues.
Playwright Rendering and Page Budget Enforcement
The script invokes Playwright’s Chromium engine to convert the normalized HTML into PDF. After rendering, countRenderedPdfPages (lines 651-682 in generate-pdf.mjs) validates the page count against the --max-pages parameter. If --strict-pages is enabled and the document exceeds the limit, the process aborts; otherwise, it emits a warning. This page-budget enforcement prevents excessively long CVs from being submitted inadvertently.
Manifest Registration
When the --report=NNN flag is provided, the function updatePDFManifest (lines 1013-1035) appends a tab-separated entry to data/pdf-index.tsv. The format follows:
[report_number]\t[pdf_path]\t[html_path]\t[format]\t[date]
Old entries for the same report number are removed first, maintaining a strict one-to-one mapping between a report and its PDF.
Linking PDFs to Evaluation Reports
The relationship between evaluation reports and their PDFs relies on a report number convention and a synchronization pipeline.
The Report Number Convention
Every evaluation report follows the naming pattern NNN-company-date.md in the reports/ directory. When generating a PDF, the same NNN identifier is passed via --report=NNN, creating a persistent link in the manifest. This convention allows downstream components to locate the correct PDF for any given report without parsing markdown files.
Tracker Synchronization via merge-tracker.mjs
The merge-tracker.mjs script (specifically lines 408-415) reads data/pdf-index.tsv and updates the PDF column in data/applications.md. This synchronization ensures that the tracker interface displays a binary status indicator (✅/❌) showing whether a PDF has been generated for each application, keeping the tracker state consistent with the filesystem.
Resolution with resolvePdfIndexPath
Components that need to access a PDF—such as the TUI dashboard or the email mode—call resolvePdfIndexPath from tracker-utils.mjs. This function loads the manifest, locates the row matching the current report number, and returns the absolute path to the PDF. This abstraction ensures that all modes consume the single source of truth stored in data/pdf-index.tsv.
Safety and Validation Mechanisms
CareerOps implements multiple guards to ensure integrity and security throughout the pipeline.
Workspace Confinement
The functions assertInsideWorkspace and isWorkspaceOutputPath validate that both input HTML and output PDF paths reside within the project’s workspace directory. This prevents path-traversal attacks where malicious input might attempt to write files outside the intended scope.
Section Order Validation
When --allow-reorder is not set, validateCvSectionOrder compares the section sequence in the rendered HTML against the canonical order defined in config/profile.yml. If divergence is detected, the script throws an error, ensuring that manually curated CV structures are not accidentally altered during the generation process.
Practical Usage Examples
Generate a PDF for a Specific Report
node generate-pdf.mjs output/cv.html output/cv.pdf \
--format=letter \
--report=018 \
--max-pages=2 \
--allow-reorder
This command produces output/cv.pdf, validates it does not exceed two pages, and registers it in the manifest under report 018.
Batch Generation Mode
node generate-pdf.mjs --batch=manifest.json \
--format=a4 \
--max-pages=1 \
--strict-pages
The batch mode processes multiple inputs defined in manifest.json, applying uniform page budgets and ATS normalization to each entry.
Programmatic PDF Lookup
import { resolvePdfIndexPath } from './tracker-utils.mjs';
const pdfPath = resolvePdfIndexPath(trackerPath);
console.log('PDF for this report:', pdfPath);
This pattern is used internally by the oferta mode (documented in modes/oferta.md) to inject PDF links into evaluation reports, and by the email mode to attach the correct file to outgoing messages.
Summary
- ATS normalization in
generate-pdf.mjsreplaces problematic Unicode characters before Playwright rendering. - The page-budget system (
--max-pages/--strict-pages) enforces length constraints viacountRenderedPdfPages. data/pdf-index.tsvserves as the central manifest, mapping report numbers to PDF paths using tab-separated values.merge-tracker.mjssynchronizes the manifest withdata/applications.md, maintaining tracker accuracy.- Workspace confinement and section-order validation prevent security issues and structural drift.
- The
ofertaevaluation report mode andemailmode both rely onresolvePdfIndexPathto consume the manifest.
Frequently Asked Questions
How does CareerOps ensure generated PDFs are ATS-compliant?
CareerOps normalizes Unicode characters that commonly break ATS parsers—such as smart quotes, em-dashes, and zero-width spaces—replacing them with ASCII equivalents before the HTML is passed to Playwright. This normalization occurs in generate-pdf.mjs prior to PDF rendering, ensuring the final document contains only safe, parseable text.
What is the purpose of the pdf-index.tsv manifest?
The manifest acts as a single source of truth that links report numbers to their corresponding PDF and HTML file paths. Stored in data/pdf-index.tsv, it allows the tracker dashboard, email mode, and evaluation reports to resolve the correct PDF path for any given application without scanning directories or parsing filenames.
How do I generate a PDF linked to a specific evaluation report?
Pass the --report flag with the report number when running generate-pdf.mjs:
node generate-pdf.mjs input.html output.pdf --report=042
This registers the PDF in the manifest under report 042, enabling automatic attachment in evaluation reports and email drafts.
What happens if a generated PDF exceeds the page budget?
If the rendered PDF exceeds the --max-pages limit and --strict-pages is enabled, the process aborts with an error. Without strict mode, the script emits a warning but continues execution. The page count is determined by countRenderedPdfPages in generate-pdf.mjs immediately after Playwright renders the document.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →