npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ajustacv-resume-schema

v0.2.0

Published

Canonical ResumeDocument schema, validators and fixtures shared by ajustacv.com and api.ajustacv.com

Readme

ajustacv-resume-schema

The canonical ResumeDocument model (issue #69), shared by the AjustaCV frontend and API so neither repo keeps its own copy.

Zod schemas are the source of truth. Every exported TypeScript type is inferred from them with z.infer, so the runtime validator and the compile-time types cannot drift apart.

Install

ajustacv.com consumes it through the Bun workspace and always builds from source in this directory, so schema edits need no release:

{ "dependencies": { "ajustacv-resume-schema": "workspace:*" } }

api.ajustacv.com is a separate repo whose Dockerfile copies only its own files, so it cannot reach this directory at build time and depends on the published version instead:

{ "dependencies": { "ajustacv-resume-schema": "^0.1.0" } }

The published tarball is source-only and carries no rights (UNLICENSED) — publishing keeps the API's Docker build credential-free, it does not make the repo open source.

Usage

import { validateResumeDocument, type ResumeDocument } from "ajustacv-resume-schema";

const result = validateResumeDocument(await request.json());
if (!result.ok) return { status: 400, issues: result.issues };

const doc: ResumeDocument = result.document;

Sample documents live behind a separate entry point so they never reach the frontend bundle:

import { FULL_RESUME, RESUME_FIXTURES } from "ajustacv-resume-schema/fixtures";

The model

ResumeDocument
├── header    ResumeHeader     name, headline, contacts, links, optional photo
├── sections  ResumeSection[]  ordered; discriminated union over 10 types
├── design    DesignSettings   templateId, free-hex accent, curated fontId, A4
└── meta      DocumentMeta     schemaVersion, targetRole, nullable ownerId

The ten section types are summary, experience, education, skills, languages, certifications, projects, achievements, strengths and custom. achievements and strengths share one entry shape — a title and a one-line description — and exist as their own types so a template can set them apart and an ATS can read them as what they are. custom is the catch-all that keeps parsing lossless — content an extractor cannot classify lands there rather than being dropped. Duplicates are allowed: a resume may carry two custom sections, or two experience ones.

summary is the single-entry section (it has entry, not entries), because a resume has one summary paragraph and a list would allow states no UI can produce. Every other section holds an array.

Conventions worth knowing

  • RichLine is one line of an inline markdown subset — **bold**, *italic*, [label](url). Block structure is modelled in the schema itself (bullets are arrays, not - prefixes), so a line break inside a RichLine means the producer flattened structure away and is rejected.
  • Dates are structured YearMonth (month optional) inside a DateRange with a current flag. An ongoing range must have a null end, and end may not precede start. Use compareYearMonth to order them; a missing month sorts as January.
  • Hiding is non-destructive. A section carries visible, and every entry carries hiddenFields. The values stay in the document and templates skip rendering them, so nothing is lost by tucking a detail away.
  • Page breaks are pageBreakBefore flags on sections and on entries.
  • URLs are restricted to http/https, and markdown links inside a RichLine to http/https/mailto. Resume content comes from parsed uploads and AI output and is rendered as hrefs in the editor and the exported PDF, and a bare z.url() accepts javascript: targets.
  • Unknown keys are stripped, not rejected, so a newer producer degrades to a valid document. Real incompatibility is caught by meta.schemaVersion.
  • Ids must be unique among sections, and among entries within a section. They anchor editor selection, undo/redo and version diffs, so a duplicate would make those features address the wrong node.

Validation

validateResumeDocument(input) returns a discriminated result rather than throwing:

{ ok: true, document: ResumeDocument } | { ok: false, issues: SchemaIssue[] }

Each issue carries a dotted path — sections.1.entries.0.dateRange.end — so the editor can anchor the error to the field that caused it. All problems are reported at once, not just the first.

Also exported: parseResumeDocumentJson for text arriving from the Draft Slot or a request body, isResumeDocument as a type guard, assertResumeDocument for call sites where invalid input is a bug rather than user input, and formatIssues for logging.

Fixtures

| Fixture | Covers | | --- | --- | | FULL_RESUME | realistic pt-BR resume, all 8 section types, a hidden field, a page break | | MINIMAL_RESUME | the smallest valid document — an untouched anonymous draft | | ALL_SECTION_TYPES_RESUME | one entry per section type, plus a hidden section | | HIDDEN_FIELDS_RESUME | every hideable field, hidden, values retained | | PAGE_BREAKS_RESUME | manual breaks at both section and entry level |

Each is authored as schema input and parsed at module load, so a fixture that falls behind a schema change fails on import instead of quietly teaching tests the wrong shape.

Releasing

The frontend builds from source, so a release is only needed when the API needs a schema change:

cd packages/resume-schema
bun test && bunx tsc --noEmit
npm version patch   # minor instead for a breaking change while on 0.x
npm publish

Then bump the range in api.ajustacv.com/package.json and run bun install there. While the version is 0.x, a caret range allows patches but not minors, so a breaking change cannot reach the API silently.

Only src ships, minus the tests — bun test and bunx tsc --noEmit must both pass before publishing, since consumers compile this source directly.