{
  "markdown": "<p align=\"center\">\n  <img src=\"assets/brand/logos/cvx-horizontal-lockup-transparent.png\" alt=\"CVX\" width=\"420\">\n</p>\n\n<p align=\"center\"><b>Structured input. Professional output. To the power of X.</b></p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/hrtips/cvx/actions/workflows/ci.yml\"><img src=\"https://github.com/hrtips/cvx/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"></a>\n  <a href=\"https://www.npmjs.com/package/@hrtips/cvx\"><img src=\"https://img.shields.io/npm/v/%40hrtips%2Fcvx\" alt=\"npm version\"></a>\n  <a href=\"https://www.npmjs.com/package/@hrtips/cvx\"><img src=\"https://img.shields.io/npm/dm/%40hrtips%2Fcvx\" alt=\"npm downloads\"></a>\n  <a href=\"https://packagephobia.com/result?p=%40hrtips%2Fcvx\"><img src=\"https://packagephobia.com/badge?p=%40hrtips%2Fcvx\" alt=\"install size\"></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/badge/license-Apache--2.0-blue\" alt=\"license\"></a>\n</p>\n\n**CVX turns plain YAML into a professionally designed CV PDF — one command, fully local, no accounts; your data never leaves your machine.** Use it two ways: edit the YAML yourself, or have any AI assistant write it for you from your existing CV — CVX validates the files and tells the assistant exactly what to fix.\n\n> **🤖 AI assistants:** if a user asked you to create their CV with CVX, follow the default flow in the [AI guide](https://raw.githubusercontent.com/hrtips/cvx/main/docs/ai-guide.md) (section \"Default flow (for assistants)\"). Non-negotiables even if you skip the link: **never invent facts** — every entry must be truthful to the user's input; **linkedin.com URLs are unfetchable even when public** — don't infer, ask for the profile's **More → Save to PDF** export or pasted text instead; ask for a **square photo** in your first reply (it can't be generated; the CV renders fine without one); and CVX is the only renderer — never substitute another PDF generator.\n\n**No terminal? No problem.** Paste this into any AI chat (Claude, ChatGPT, Gemini, …):\n\n```text\nCreate my CV with https://github.com/hrtips/cvx — open that page and follow its instructions for AI assistants.\nHere is my LinkedIn profile: <your profile URL> (or attach your current CV and a photo instead).\n```\n<p align=\"center\">\n  <img src=\"assets/hero-two-column.png\" alt=\"Designed two-column CV produced by CVX — teal theme, photo sidebar, achievements\" width=\"720\">\n</p>\n<p align=\"center\"><sub>Page 1 of the scaffolded example CV — plus an <a href=\"assets/hero-ats.png\">ATS-safe single-column variant</a> via <code>--ats</code>. Pagination has improved since these were captured; your build may split pages differently.</sub></p>\n\n---\n\n## Create your CV in two minutes\n\n```bash\nnpx @hrtips/cvx init     # scaffold cv-content/ with a complete example CV\nnpx @hrtips/cvx build    # render it to a PDF\n```\n\n`init` gives you a finished, working CV — **Bruce Wayne's**, and yes, really. Open `bruce-wayne.pdf` and you're looking at a designed two-page CV: photo, sidebar, achievements, the lot.\n\nNow make it yours. Open the `cv-content/` folder, and replace Bruce's details with your own, one file at a time:\n\n```\ncv-content/\n  personal.yaml       ← start here: your name, title, contact details\n  summary.yaml        ← the bullet points at the top of page 1\n  experience.yaml     ← your work history (the bulk of the CV)\n  education.yaml      ← degrees, institutions, years\n  competencies.yaml   ← skill pills in the sidebar\n  achievements.yaml   ← awards and recognitions\n  referees.yaml       ← referees, or [] for \"available upon request\"\n  images/profile.jpg  ← your photo (square, 400×400px or larger)\n```\n\nRe-run `npx @hrtips/cvx build` after each file and watch the PDF update — seeing Bruce's entry next to yours makes the format self-explanatory. The output file is named after you automatically (`jane-doe.pdf`).\n\nMade a typo or unsure a file is right? `npx @hrtips/cvx validate` checks everything at once and tells you exactly what to fix:\n\n```\ncv-content/personal.yaml\n  ⚠ unknown key \"linkdin\"\n      ↳ did you mean \"linkedin\"?\n```\n\nEvery scaffolded file also carries a `$schema` header, so editors with YAML support (VS Code + the YAML extension, JetBrains, …) autocomplete keys and flag mistakes as you type.\n\nApplying through a job portal? Generate the ATS-safe variant too — single column, no colours, machine-friendly:\n\n```bash\nnpx @hrtips/cvx build --ats\n```\n\n### CLI reference\n\n| Command | Does |\n|---|---|\n| `npx @hrtips/cvx init` | Scaffold `cv-content/` with the example CV (won't overwrite an existing one) |\n| `npx @hrtips/cvx validate` | Check `cv-content/` — every problem at once, with file + field paths and fixes |\n| `npx @hrtips/cvx validate --strict` | Also fail on warnings (unknown keys); recommended for agents/CI |\n| `npx @hrtips/cvx build` | Render `cv-content/` to `<your-name>.pdf` |\n| `npx @hrtips/cvx build --ats` | Render the ATS-safe single-column variant |\n| `npx @hrtips/cvx list` | Show available themes and layouts |\n| `npx @hrtips/cvx --help` / `--version` | Help / version |\n\nAll commands accept `--json` for machine-readable output (one JSON object on stdout, logs on stderr) and use semantic exit codes: `0` ok, `2` validation failed, `3` render failed, `64` usage error. `init` is a convenience, not a prerequisite — `build` renders any `cv-content/` folder with valid YAML (built-in themes and layouts need no extra files).\n\n**Content schema:** content files are versioned by `schemaVersion` in `config.yaml` (currently `1`) and validated against the [canonical JSON Schema](schema/v1/cvx.schema.json). New keys may appear within a schema major. Keys can also be **removed** when they are measured to do nothing — `page1ExperienceCount` and `page1SplitBullets` were, in 1.8.0 — in which case builds keep working and `validate` names the removal, while `validate --strict` treats the now-unknown key as an error. The CHANGELOG says so at the release that does it.\n\n---\n\n## What goes in each file\n\nThese are excerpts from the scaffolded example — build it once and you can see exactly where each snippet lands on the page.\n\n**personal.yaml** — the header and contact block:\n```yaml\nname: Bruce Wayne\ntitle: Founder & Field Commander – Gotham Operations\ncompany: Wayne Enterprises\nphone: \"+1 (201) 555-2283\"\nphoneHref: \"tel:+12015552283\"\nemail: bruce.wayne@wayne-enterprises.com\nlinkedin: linkedin.com/in/brucewayne\nlinkedinHref: \"https://www.linkedin.com/in/brucewayne\"\n```\n\n**experience.yaml** — one entry per role; `progression` (optional) renders a title history inside the entry:\n```yaml\n- role: Founder & Field Commander – Gotham Operations\n  company: The Batman\n  period: 2005 – Present\n  description: Self-directed vigilante operation safeguarding Gotham City through deterrence, investigation, and crisis response.\n  progression:\n    - title: Commander, Batman Incorporated\n      period: 2011 – Present\n    - title: Solo Operative, The Dark Knight\n      period: 2005 – 2008\n  bullets:\n    - Established and scaled a citywide security operation from a solo initiative to a franchised network (Batman Incorporated).\n    - Recruited, trained, and led a high-performing field team including Robin, Nightwing, and Batgirl.\n    - Reduced organised-crime activity in Gotham by an estimated 60% through data-driven surveillance and rapid incident response.\n```\n\n**summary.yaml** — the bullet list at the top of page 1:\n```yaml\n- \"Strategic operations leader with 20+ years' experience, progressing from solo field operative to Field Commander of a citywide security network.\"\n- \"Co-founded the Justice League as a global response coalition, serving as chief strategist and contingency planner for existential-scale threats.\"\n```\n\n**education.yaml**\n```yaml\n- degree: \"Applied Sciences & Criminology (self-directed)\"\n  institution: League of Shadows\n  period: 1998 – 2004\n\n- degree: BSc, Criminology & Chemistry\n  institution: Gotham University\n  period: 1994 – 1998\n```\n\n**competencies.yaml** — rendered as skill pills in the sidebar:\n```yaml\n- Strategic Planning\n- Criminal Investigation\n- Crisis Response\n- Surveillance & Intelligence\n```\n\n**achievements.yaml**\n```yaml\n- year: Gotham's Most Influential Citizen\n  text: \"— 2024, Gotham Gazette\"\n\n- year: Key to the City\n  text: \"— Office of the Mayor, Gotham City\"\n```\n\n**referees.yaml** (use `[]` to print \"available upon request\"):\n```yaml\n- name: Diana Prince\n  title: Founding Member, Justice League\n  company: Themysciran Embassy\n  email: d.prince@justiceleague.org\n  phone: \"+1 (202) 555-0177\"\n```\n\nDelete what you don't need — an empty file (or `[]`) simply drops that section from the CV. Any new `.yaml` file you drop into `cv-content/` is auto-discovered as a content key.\n\nThe complete field-by-field schema for every file lives in [docs/cv-schema.md](docs/cv-schema.md).\n\n### Let an AI write the YAML for you\n\nThe formats are deliberately LLM-friendly, and the schema is published for machines ([llms.txt](llms.txt), [docs/cv-schema.md](docs/cv-schema.md)). **[The AI guide](docs/ai-guide.md)** has copy-paste prompts for every route; the short version:\n\n**Coding agent** (Claude Code, Cursor, …) — lowest friction: run `npx @hrtips/cvx init`, then ask the agent to replace the example content with your details and build. The scaffolded `cv-content/README.md` documents the schema, so the agent edits, runs `npx @hrtips/cvx build`, and fixes errors itself.\n\n**Chat assistant** (Claude, ChatGPT, …) — paste your existing CV or LinkedIn profile text along with this prompt:\n\n> Read the CVX content schema at https://raw.githubusercontent.com/hrtips/cvx/main/docs/cv-schema.md then convert my CV below into CVX cv-content/ YAML files. Output each file in its own fenced code block titled with the filename. Keep every fact truthful to my input — don't invent anything.\n\nSave the generated files into `cv-content/`, drop in your photo, run `npx @hrtips/cvx build`. No web access in your assistant? Use the [self-contained prompt](docs/ai-guide.md#route-c--chat-assistant-self-contained-prompt).\n\n### Plug it into your agent (MCP)\n\nCVX ships an MCP server — any MCP client (Claude Desktop, Claude Code, Cursor, VS Code, …) can drive the whole loop with five tools: `get_schema`, `init_cv`, `validate_cv`, `build_pdf`, and `plan_layout` (a dry run that reports how the CV paginates — page count, per-page column fills, what landed where — without writing a PDF). No API keys, fully offline.\n\nCVX renders 100% of your YAML and never drops, clips, or hides text to fit a page — so an assistant driving it can't quietly cut a section to hit a page count either. If the CV runs longer than you want, it surfaces the trade-off and you decide what goes.\n\n```bash\nnpx @hrtips/cvx mcp init --client claude          # Claude Code (.mcp.json, project)\nnpx @hrtips/cvx mcp init --client claude-desktop  # Claude Desktop (global config)\nnpx @hrtips/cvx mcp init --client cursor          # Cursor (.cursor/mcp.json)\nnpx @hrtips/cvx mcp init --client vscode          # VS Code (.vscode/mcp.json)\n```\n\nThen restart the client and ask it to make your CV — it fetches the schema, scaffolds, fills in your details, validates after every edit, and renders the PDF. The config writer merges into existing files; it never clobbers other servers. There's also a ready-made [Agent Skill](skills/cvx/SKILL.md) with the same loop for skill-capable agents.\n\n### Build it into a ChatGPT GPT\n\nChatGPT's sandbox has a Node runtime but usually cannot reach the npm registry, so `npx` does not work there. CVX therefore also ships as **one self-contained file** — schema, template, fonts and all — attached to [every release](https://github.com/hrtips/cvx/releases/latest) as `cvx.bundle.min.js`. It needs nothing but Node 20+: no install, no `node_modules`, no further downloads.\n\n**In most ChatGPT sandboxes it can fetch the file itself**, so you upload nothing — the [Route D prompt](docs/ai-guide.md#route-d--agent-mode-assistant-zero-local-setup) gives it the four setup commands verbatim, which matters because an assistant left to work it out will try `npx` first and waste your turn. If the download is blocked, download the 0.92 MB zip and upload it instead; everything after that is identical.\n\nThe GPT does what a chat assistant otherwise cannot: it renders the PDF, **opens it and looks at the pages**, then fixes the layout before you ever see it.\n\n### Your photo\n\nDrop it into `cv-content/images/` as `profile.<ext>` — `jpg`, `jpeg`, `png`, or `webp` are auto-detected (that order wins if several exist). Square crop, at least 400×400px.\n\n---\n\n## Themes, layouts, and page flow\n\nEverything visual is controlled by `cv-content/config.yaml`:\n\n```yaml\ntheme: teal               # teal | coral | mono\nlayout: two-column        # two-column | single-column\n```\n\nChange a value, re-run `npx @hrtips/cvx build`, done.\n\n**Themes** control colour and styling:\n\n| Theme | Accent | Description |\n|---|---|---|\n| `teal` | `#1a6070` | Professional teal (default) |\n| `coral` | `#c0534a` | Warm coral red |\n| `mono` | `#000000` | Black and white, ATS-optimised |\n\n**Layouts** control page structure:\n\n| Layout | Structure | Description |\n|---|---|---|\n| `two-column` | Sidebar + main column | Designed CV with photo, identity block, achievements |\n| `single-column` | Full width | ATS-safe, no sidebar, no decorative elements |\n\n**Pagination** — experience entries are distributed across pages automatically (greedy bin-packing), never overflowing a page, and an entry too tall for the remaining room is split at a bullet boundary and continued overleaf. There are no pagination settings: the layout follows the content. (The old `page1ExperienceCount` / `page1SplitBullets` keys were removed — measured, they never reduced the page count, and forcing them pushed content onto an unnumbered extra sheet. A config that still has them gets a validation message saying exactly that.)\n\n### Script support\n\n**CV rendering is English/Western-European Latin only.** CVX bundles [Lato](https://fonts.google.com/specimen/Lato) and registers no fallback font, so scripts Lato doesn't cover — Cyrillic, Greek, Vietnamese, Turkish `ş`/`ğ`, Czech/Romanian diacritics, and all non-Latin scripts (Devanagari, Tamil, Sinhala, CJK, Arabic, …) — render invisibly. `cvx validate` and `cvx build` warn loudly when your content contains characters the bundled font cannot draw, so this fails visibly rather than silently.\n\nThis is a deliberate scope decision, not an oversight: shipping fallback fonts for those scripts would blow the package-size budget many times over. (The project website is multilingual; the renderer is not — the two are independent.)\n\n### Custom layouts\n\nYou can define your own page structure — drop a `.yaml` file into `cv-content/layouts/` and reference it by filename:\n\n```yaml\n# cv-content/layouts/compact.yaml\ntemplate: two-column\n\npages:\n  first:\n    sidebar:\n      - identity-photo\n      - contact\n    main:\n      - summary\n      - spacer: 27\n      - experience\n\n  continuation:\n    sidebar:\n      - identity-compact\n      - education\n      - competencies\n      - achievements\n    main:\n      - experience:continued\n\n  last:\n    sidebar:\n      - identity-compact\n      - referees\n    main:\n      - experience:continued\n```\n\nThen set `layout: compact` in `config.yaml`.\n\n**The sidebar's three lists are one ordered flow, not three page assignments.**\n`first.sidebar` + `continuation.sidebar` + `last.sidebar` are concatenated in\nthat order, and CVX measures the result to decide which page each section lands\non. `last.sidebar: [referees]` therefore means *\"referees comes last in the\nsidebar\"*, not *\"referees renders on the last page\"*. Use the buckets to express\n**order**; let pagination be measured. (`identity-photo`/`identity-compact` are\nthe exception — they are injected at the top of every page's sidebar rather than\npacked.) The **main** lists remain per-page-kind: `first.main` on page 1,\n`last.main` on the final page, `continuation.main` in between.\n\nAvailable section keys:\n\n| Key | Renders |\n|---|---|\n| `identity-photo` | Name, title, company + profile photo (sidebar) |\n| `identity-compact` | Name, title, company without photo (sidebar) |\n| `contact` | Phone, email, LinkedIn, location with icons (sidebar) |\n| `achievements` | Year + description list (sidebar) |\n| `education` | Degree, institution, period (sidebar) |\n| `competencies` | Skill tags as pills (sidebar) |\n| `referees` | Referee contact details (sidebar) |\n| `summary` | Bullet point list (main column) |\n| `experience` | Experience entries for page 1 (main column) |\n| `experience:continued` | Continuation experience entries (main column) |\n| `header-ats` | Full-width name/title/contact header (single-column) |\n| `spacer: N` | Vertical spacer of N points |\n\n---\n\n## ATS & AI-parser keywords\n\nBoth PDFs embed a keyword list into the standard **`Keywords` metadata field** — the field some applicant tracking systems (ATS) and AI CV parsers read. Keywords live in metadata, **not** as hidden text on the page.\n\n> **Reality check:** most mainstream ATS rank on text extracted from the CV *body*, and support for the PDF `Keywords`/XMP field is inconsistent. Treat this as a best-effort supplement to a keyword-rich body — not a substitute, and not a reliable ranking lever on its own. Keep every keyword **truthful**; stuffing false or irrelevant terms causes a metadata/body mismatch that gets a CV auto-rejected.\n\nKeywords come from two sources, merged and de-duplicated (body-derived terms first):\n\n1. **Auto-derived** from your `competencies.yaml` and job titles (company names are deliberately excluded as low-signal).\n2. **Curated** in `cv-content/keywords.yaml` — a flat list, or grouped under headings:\n\n```yaml\n# cv-content/keywords.yaml\n- Operations Management\n- Risk Management\n# …or grouped:\nLeadership: [Executive Leadership, Team Building]\n```\n\nConfigure in `config.yaml`:\n\n```yaml\natsKeywords:\n  enabled: true       # master switch                                  (default: true)\n  autoDerive: true    # also derive from competencies + job titles      (default: true)\n  max: 40             # optional cap; body-derived terms are kept first (default: all)\n```\n\n---\n---\n\n# For developers\n\nEverything below is about hacking on CVX itself — custom themes, the rendering pipeline, and contributing. You don't need any of it to create a CV.\n\n## Working from a clone\n\n```bash\ngit clone git@github.com:hrtips/cvx.git\ncd cvx\nnpm install\nnpm run dev        # live browser preview at http://localhost:5173\nnpm run pdf        # generate PDF (same pipeline as `cvx build`)\nnpm run pdf:ats    # generate the ATS variant\nnpm test           # unit tests\nnpm run build:lib  # build the publishable lib/ (what the CLI runs)\n```\n\nThe repo's `cv-content/` carries the same Bruce Wayne example, so a clone builds out of the box. A photo crop helper is included: `python3 scripts/crop-profile.py path/to/photo.jpg`.\n\n## Custom themes\n\n> Themes ship inside the package, so custom themes currently require working from a clone — the `npx` CLI offers the three built-ins.\n\nDrop a `.js` file in `src/pdf/themes/` — it's auto-discovered, no registration needed:\n\n```js\n// src/pdf/themes/navy.js\nimport { tealTheme } from './teal.js'\n\nexport const navyTheme = {\n  ...tealTheme,\n  name: 'navy',\n  palette: {\n    ...tealTheme.palette,\n    accent:    '#1e3a5f',\n    sidebarBg: '#eef1f5',\n    tagBg:     '#d0d8e8',\n    tagText:   '#1e3a5f',\n    divider:   '#b8c4d4',\n  },\n}\n```\n\nSet `theme: navy` in `config.yaml`. The theme object has five namespaces you can override:\n\n| Namespace | Controls |\n|---|---|\n| `palette` | All colours — accent, backgrounds, text, tags, dividers, semantic opacity colours |\n| `typography` | Font sizes, weights, letter spacing, line heights per element |\n| `spacing` | Gaps, margins, padding values used by all components |\n| `chrome` | Decorative dimensions — border radii, divider widths, photo size, corner badge |\n| `geometry` | Page dimensions, column fractions, padding objects |\n\n## Auto-discovery\n\nConvention over registration — drop a file in the right folder and it's picked up:\n\n| What | Where to drop it | How it's found |\n|---|---|---|\n| **Content** | `cv-content/*.yaml` | Any `.yaml` file (except `config.yaml`) becomes a content key matching its filename |\n| **Themes** | `src/pdf/themes/*.js` | Any `.js` file exporting an object with a `name` property |\n| **Layouts** | `cv-content/layouts/*.yaml` | Any `.yaml` file with a `template` and `pages` structure |\n| **Profile photo** | `cv-content/images/profile.*` | First match of `.jpg`, `.jpeg`, `.png`, `.webp` |\n\nTo add a whole new content section (e.g. publications): create `cv-content/publications.yaml` (auto-available as `content.publications`), build a section component in `src/pdf/sections/`, register it in `registry.js`, then reference it from a layout.\n\n## Architecture\n\nThree concerns, independently swappable:\n\n```\nContent (YAML)  ×  Theme (JS)  ×  Layout (YAML)\n     ↓                ↓               ↓\n  what to say    how it looks    where it goes\n```\n\nThe rendering pipeline:\n\n```\nconfig.yaml → resolves theme + layout\n    ↓\ncv-content/*.yaml → auto-loaded into data bag\n    ↓\nlayout.js → packs experience entries across pages (using theme metrics)\n    ↓\nCVDocument → picks template (TwoColumn or SingleColumn)\n    ↓\ntemplate → renders slots from layout config via section registry\n    ↓\nsections → read theme from React context, render content\n    ↓\n@react-pdf/renderer → PDF buffer → file\n```\n\n## Project structure\n\n```\ncv-content/                      ← content (the repo carries the example CV)\n  *.yaml                         ← auto-discovered content files\n  config.yaml                    ← theme, layout, pagination\n  layouts/                       ← auto-discovered layout definitions\n  images/profile.<ext>           ← photo (jpg/jpeg/png/webp)\n\nsrc/pdf/                         ← framework\n  CVDocument.jsx                 ← unified config-driven document\n  ATSDocument.jsx                ← standalone ATS document\n  ThemeContext.jsx               ← React context + useStyles hook\n  render.js                      ← shared render pipeline (CLI + scripts)\n  layout.js                      ← theme-aware page-packing + sidebar resolution\n  keywords.js                    ← ATS/AI-parser keyword metadata\n  reproducible.js                ← SOURCE_DATE_EPOCH support (see below)\n  profilePhoto.js                ← shared profile-image extension precedence\n  loadContent.js                 ← auto-discovers cv-content/*.yaml\n  loadLayout.js                  ← normalizes layout YAML → slot config\n  fonts.js                       ← Lato font registration\n  *.test.js                      ← Vitest unit tests (co-located)\n  themes/                        ← auto-discovered themes (teal, coral, mono)\n  templates/                     ← page shells (TwoColumn, SingleColumn)\n  sections/                      ← content sections + registry\n  components/                    ← shared leaf components\n\nscripts/\n  export-pdf.js                  ← npm run pdf\n  export-pdf-ats.js              ← npm run pdf:ats\n  build-lib.js                   ← npm run build:lib (publishing build)\n  crop-profile.py                ← photo crop helper\n\nbin/cvx.js                    ← npx CLI (init / build)\ntemplate/cv-content/             ← starter content scaffolded by `cvx init`\nlib/                             ← generated: published transform of src/pdf (gitignored)\n```\n\n## Testing\n\n```bash\nnpm test\n```\n\nUnit tests cover the pure logic modules — keyword building, page packing, sidebar resolution, reproducibility helpers, and the profile-photo picker. Tests live next to the code they cover (`src/pdf/*.test.js`) plus `test/` for cross-cutting checks. CI runs the suite on Linux and macOS across Node 20/22/24 (plus Windows on Node 22), then packs the tarball and exercises the installed CLI end-to-end, including a byte-identical reproducibility gate.\n\n## Quality gates\n\nThe build is deliberately hostile — a change lands only if it clears every gate below with **zero warnings**. Run them all locally with `npm run check` (lint + types + coverage + dead-code); CI enforces them in the `quality` and `security` jobs.\n\n| Gate | Command | Enforces |\n|---|---|---|\n| Lint + format | `npm run lint` | [Oxlint](https://oxc.rs) (fast correctness/`suspicious`, incl. `rules-of-hooks`, `no-focused-tests`) **+** [Biome](https://biomejs.dev) (format, imports, deeper lint) — zero warnings |\n| Types | `npm run typecheck` | `tsc --noEmit` with `checkJs` + full `strict` over the JS/JSX sources (types via JSDoc + `src/pdf/types.d.ts`, derived from the JSON schema) |\n| Coverage | `npm run test:cov` | [Vitest](https://vitest.dev) v8, **per-file ≥ 90%** lines/functions/statements, ≥ 85% branches — no global averaging, no per-file waivers |\n| Dead code | `npm run knip` | no unused files, exports, or dependencies |\n| Package | `npm run publint` · `npm run attw` | [publint](https://publint.dev) packaging + [arethetypeswrong](https://arethetypeswrong.github.io) resolution |\n| Supply chain | `security` CI job | [actionlint](https://github.com/rhysd/actionlint) · [zizmor](https://docs.zizmor.sh) (SHA-pinned actions, least-privilege) · [gitleaks](https://gitleaks.io) · [osv-scanner](https://google.github.io/osv-scanner/) |\n\nFormatting/lint autofix: `npm run lint:fix`. GitHub Actions are pinned to commit SHAs and kept current by Dependabot.\n\n## Reproducible builds\n\nSet [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) (seconds since the Unix epoch) to make PDF output byte-identical run after run — useful for CI checks and for verifying that a content change is the *only* thing that changed:\n\n```bash\nSOURCE_DATE_EPOCH=$(git log -1 --format=%ct) npx @hrtips/cvx build   # or npm run pdf\n```\n\nThis pins the PDF's `CreationDate` (and with it the trailer file ID), the font-subset names, and the object write order — the things that otherwise vary per run. Unset, builds behave normally and stamp the current time.\n\nByte-identical output is guaranteed for the same platform and Node version. Builds from different OSes or Node majors are visually identical but not byte-identical (font subsets embed in a platform-dependent order, and zlib output varies across Node versions).\n\n## Tech stack\n\n- **[@react-pdf/renderer](https://react-pdf.org/)** — renders React to PDF (no headless browser)\n- **[Vite](https://vitejs.dev/) + React** — live browser preview\n- **[js-yaml](https://github.com/nodeca/js-yaml)** — YAML parsing (pinned to 4.x to match `@rollup/plugin-yaml`)\n- **[esbuild](https://esbuild.github.io/)** — transform-only build of `lib/` for publishing\n- **[tsx](https://github.com/privatenumber/tsx)** — runs the repo export scripts\n- **[Vitest](https://vitest.dev/)** — unit tests\n- **[Lato](https://fonts.google.com/specimen/Lato)** — embedded font (Light 300, Regular 400, Bold 700)\n\n---\n\n## License\n\n[Apache-2.0](LICENSE) © ramith.\n\nThe bundled [Lato](https://fonts.google.com/specimen/Lato) fonts are licensed separately under the [SIL Open Font License 1.1](https://openfontlicense.org/open-font-license-official-text/).\n",
  "bytes": 27116,
  "sha": "23a2d7adce86b8a86f3b5f5064be4b184a6084172bb33843d57225a82f282eb4",
  "repo_slug": "hrtips/cvx",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_hrtips_cvx_567af1f2/readme"
}