resumectl 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GLOSSARY.md +19 -0
- package/LICENSE +21 -0
- package/README.md +63 -2
- package/docs/adr/0001-domain-first-organization.md +32 -0
- package/docs/agents/domain.md +12 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +11 -0
- package/docs/api.md +16 -0
- package/docs/configuration.md +83 -0
- package/docs/quality.md +36 -0
- package/docs/rendering.md +68 -0
- package/docs/sans.png +0 -0
- package/docs/serif.png +0 -0
- package/docs/setup.md +31 -0
- package/package.json +48 -4
- package/resume.config.schema.json +85 -0
- package/resume.config.yaml +65 -0
- package/skills/resumectl/SKILL.md +143 -0
- package/src/cli/main.ts +108 -0
- package/src/cli.ts +14 -0
- package/src/content/tex.ts +107 -0
- package/src/errors.ts +3 -0
- package/src/index.ts +22 -0
- package/src/project/config.ts +265 -0
- package/src/project/doctor.ts +76 -0
- package/src/project/init.ts +78 -0
- package/src/quality/gates.ts +113 -0
- package/src/quality/pdf.ts +84 -0
- package/src/rendering/assets.ts +4 -0
- package/src/rendering/compile.ts +101 -0
- package/src/rendering/font-metrics.ts +89 -0
- package/src/rendering/font-width.ts +11 -0
- package/src/rendering/measurements.ts +81 -0
- package/src/rendering/metrics/sans.json +122 -0
- package/src/rendering/metrics/serif.json +121 -0
- package/template/fonts/Inter-430.ttf +0 -0
- package/template/fonts/Inter-500.ttf +0 -0
- package/template/fonts/Inter-600.ttf +0 -0
- package/template/fonts/OFL-Inter.txt +93 -0
- package/template/resume-measure.tex +74 -0
- package/template/resume.cls +203 -0
package/GLOSSARY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
resumectl has one domain context: authoring, rendering and checking a résumé from verified career evidence.
|
|
4
|
+
|
|
5
|
+
| Term | Meaning | Owner |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Source | The `.tex` document and recursively included `.tex` content | `src/content/tex.ts` |
|
|
8
|
+
| Section | A normalized heading used to match content with its editing/checking policy | `src/content/tex.ts` |
|
|
9
|
+
| Entry | One measured list item, prose paragraph, or table description; may contain several paragraphs | `src/rendering/measurements.ts` |
|
|
10
|
+
| Bullet | A list entry; legacy API names also use `Bullet` for other measured entries | `src/rendering/measurements.ts` |
|
|
11
|
+
| Effective config | Values resolved in order: CLI/API overrides > YAML > documentclass > defaults | `src/project/config.ts` |
|
|
12
|
+
| Editing policy | A section's `editable` guidance for an agent; distinct from whether length checks run | `src/project/config.ts` |
|
|
13
|
+
| Layout | Style, paper, margins, spacing and font settings used for compilation | `src/project/config.ts` |
|
|
14
|
+
| Render manifest | `<name>.resumectl.json`: actual geometry, layout, source/PDF hashes and overflow findings | `src/rendering/measurements.ts` |
|
|
15
|
+
| Last-line fill | Final paragraph's rendered text width divided by its available line width | `src/rendering/measurements.ts` |
|
|
16
|
+
| Quality gate | A pass/fail rule for content length/overflow, page budget or PDF private-use mappings | `src/quality/gates.ts` |
|
|
17
|
+
| Writing evidence | User-supplied facts supporting ownership, mechanisms and outcomes; not inferred from layout scores | `skills/resumectl/SKILL.md` |
|
|
18
|
+
|
|
19
|
+
See [the architecture decision](docs/adr/0001-domain-first-organization.md) for module boundaries.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick Huo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# resumectl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A harness for agent-written résumés.**
|
|
4
|
+
|
|
5
|
+
Paste in a job description and your background. Your coding agent (Claude Code, Codex, Cursor and similar) writes a tailored one-page résumé in LaTeX, renders it, and keeps revising until the PDF passes every check: it fits the page, has no ragged last lines, and parses cleanly in an ATS. Every claim comes from facts you gave it, so it can't invent metrics.
|
|
6
|
+
|
|
7
|
+
| Sans (recommended) | Serif |
|
|
8
|
+
|---|---|
|
|
9
|
+
|  |  |
|
|
10
|
+
|
|
11
|
+
<sub>Example people, schools and companies are fictional.</sub>
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
- **LaTeX, not Word.** Typesetting is far better, and you never fight Word's interface.
|
|
16
|
+
- **LaTeX suits agents, with a harness to keep them in line.** A `.tex` file is plain text, so an agent can edit it precisely. Left alone, though, it overflows the page and pads bullets. resumectl measures the rendered PDF and makes the build fail until the content fits.
|
|
17
|
+
- **Plain by design.** No icons, colour bars or two-column gimmicks. There are two typefaces with carefully tuned sizes and weights. I prefer the sans because it's easier to read on screen, where résumés are actually read. Both styles are single-column, real text and ATS-friendly.
|
|
18
|
+
|
|
19
|
+
## Get started
|
|
20
|
+
|
|
21
|
+
You need [Bun](https://bun.sh) ≥ 1.3. Everything else is set up by your agent.
|
|
22
|
+
|
|
23
|
+
**1. Install the skill:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx skills add nickhuo/resumectl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It works with Claude Code, Codex, Cursor, Copilot and [other agents](https://skills.sh).
|
|
30
|
+
|
|
31
|
+
**2. Ask your agent:**
|
|
32
|
+
|
|
33
|
+
> Make me a résumé for this job.
|
|
34
|
+
>
|
|
35
|
+
> Job description: *(paste)*
|
|
36
|
+
>
|
|
37
|
+
> My background: *(paste your old résumé, LinkedIn, notes, project write-ups)*
|
|
38
|
+
|
|
39
|
+
On the first run the agent checks your setup. If you have no TeX installed, it asks before installing [Tectonic](https://tectonic-typesetting.github.io), a 16 MB typesetter. The first build then downloads about 45 MB of packages, which takes a couple of minutes; later builds take seconds.
|
|
40
|
+
|
|
41
|
+
The agent then maps the job's requirements to your evidence and asks about gaps instead of guessing. It writes the résumé, builds the PDF, and revises until every check passes.
|
|
42
|
+
|
|
43
|
+
## What the build checks
|
|
44
|
+
|
|
45
|
+
| Check | Default |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Page budget | 1 page |
|
|
48
|
+
| Entry length | Up to 2 lines per bullet, last line at least 80% full, no overflow |
|
|
49
|
+
| Copyable text | Text copied out of the PDF matches the page (phone numbers, names, brackets) |
|
|
50
|
+
|
|
51
|
+
You can change any limit per section in `resume.config.yaml`, and mark sections such as Education as off-limits to the agent. See [configuration](docs/configuration.md).
|
|
52
|
+
|
|
53
|
+
## Editing by hand
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bunx resumectl init ./my-resume --style sans # starter resume.tex + config
|
|
57
|
+
bunx resumectl build ./my-resume/resume.tex # PDF in my-resume/output/, all checks
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Fonts, sizes, margins and LaTeX commands are documented in [rendering](docs/rendering.md).
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
[MIT](LICENSE). Inter is bundled under the [SIL Open Font License](template/fonts/OFL-Inter.txt). TeX Gyre Termes comes from your TeX distribution.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 0001 — Organize by résumé capabilities
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-16
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The project already renders LaTeX, resolves configuration, measures entries and checks PDFs. A flat `src/` obscured those responsibilities, and the writing workflow lived in a personal Career repository. The product needs a short path from career evidence to a checked résumé without introducing separate services or multiple executables.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
Keep one domain context and one package. Group code by capability:
|
|
13
|
+
|
|
14
|
+
- `content`: interpret source, included files, section keys and visible text. Depends only on shared input errors and platform APIs.
|
|
15
|
+
- `project`: resolve configuration, create starter projects and diagnose toolchain availability. Uses content parsing and rendering asset paths; does not compile or invoke quality gates.
|
|
16
|
+
- `rendering`: compile via XeLaTeX, capture actual geometry, manage manifests and own font utilities. Uses effective project configuration and source expansion.
|
|
17
|
+
- `quality`: evaluate rendered measurements and PDF facts. Uses content parsing, project policy validation and rendering manifests; does not invoke the compiler.
|
|
18
|
+
- `cli`: parse arguments, compose the capabilities, select human/JSON output and map outcomes to exit codes.
|
|
19
|
+
|
|
20
|
+
These are capability modules within one context, not separate bounded contexts. Project and rendering share configuration/asset contracts, but no cyclic file imports are required. Do not add repository/service/controller layers just for symmetry. Common errors remain at `src/errors.ts`; package consumers use `src/index.ts`.
|
|
21
|
+
|
|
22
|
+
Retain `src/cli.ts`, root API exports, `template/`, the config schema and existing command names. Move legacy width estimation to rendering; quality gates continue to use measured geometry.
|
|
23
|
+
|
|
24
|
+
Expose only the essential workflow through `resumectl`: `init`, `doctor`, `config init/show`, `build`, `check`. Add `bun run resumectl` for repository use and `bun run verify` for development checks. `init` refuses overwrites; `doctor` diagnoses without installing dependencies. All workflow commands support JSON. Keep font metric regeneration as a maintenance script.
|
|
25
|
+
|
|
26
|
+
Store writing instructions in `skills/resumectl/SKILL.md` and include `skills/` in the package. Adapt the local methodology to verified candidate facts, per-section edit policy and current rendered quality gates. Remove hard-coded personal claims and obsolete character-count gates. Do not create a second résumé data format.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
Tests mirror capabilities; integration tests still compile both styles and check output. Documentation uses one root glossary and root ADR directory, with separate rendering, quality and configuration references. Moving internal source paths is intentional; root package exports remain the supported API.
|
|
31
|
+
|
|
32
|
+
A split into multiple packages, a generic DDD framework and independent CLIs would add coordination without a separate lifecycle or data owner, so they are deferred until an actual requirement appears.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Domain documentation
|
|
2
|
+
|
|
3
|
+
This is a single-context project. Maintain vocabulary in [`GLOSSARY.md`](../../GLOSSARY.md) and decisions in [`docs/adr/`](../adr/).
|
|
4
|
+
|
|
5
|
+
- Follow [ADR 0001](../adr/0001-domain-first-organization.md) when placing code.
|
|
6
|
+
- Put behavior and its tests with the owning capability. `src/cli/main.ts` composes commands; it does not own configuration, measurement or writing rules.
|
|
7
|
+
- Use actual rendered geometry for quality decisions. Keep legacy font-width utilities out of the gate calculation.
|
|
8
|
+
- Keep `.tex` as the source of résumé content and the manifest as generated evidence, not editable content.
|
|
9
|
+
- Keep root package exports stable when moving internal files.
|
|
10
|
+
- Run `bun run verify`; rendering tests require the environment described in [`setup.md`](../setup.md).
|
|
11
|
+
|
|
12
|
+
The [writing skill](../../skills/resumectl/SKILL.md) owns résumé-writing guidance, not the TypeScript compiler/checker implementation.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Issue tracker
|
|
2
|
+
|
|
3
|
+
Use GitHub Issues for `nickhuo/resumectl` via `gh`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gh issue list --repo nickhuo/resumectl
|
|
7
|
+
gh issue view <number> --repo nickhuo/resumectl
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Use the existing issue when one is supplied. Issue descriptions should identify the affected domain, expected behavior and a verifiable acceptance condition. See [triage labels](triage-labels.md) for readiness. Do not mirror issues into another tracker.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Triage labels
|
|
2
|
+
|
|
3
|
+
| Label | Meaning |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `needs-triage` | Scope and ownership have not been assessed |
|
|
6
|
+
| `needs-info` | Required facts or reproduction details are missing |
|
|
7
|
+
| `ready-for-agent` | Work is concrete and has a verifiable acceptance condition |
|
|
8
|
+
| `ready-for-human` | A human decision or action is required |
|
|
9
|
+
| `wontfix` | The project will not implement the request |
|
|
10
|
+
|
|
11
|
+
Use these existing names rather than introducing synonymous labels. Confirm issue context before changing its status.
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Library API
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { compile, runGates, formatReport, resolveConfig } from "resumectl";
|
|
5
|
+
|
|
6
|
+
const { config } = await resolveConfig({ texPath: "resume.tex" });
|
|
7
|
+
const { pdfPath } = await compile("resume.tex", {
|
|
8
|
+
layout: config.layout, outDir: config.build.outDir,
|
|
9
|
+
});
|
|
10
|
+
const report = await runGates("resume.tex", pdfPath, {
|
|
11
|
+
...config.checks, sections: config.sections,
|
|
12
|
+
}, config.layout);
|
|
13
|
+
console.log(formatReport(report));
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The package root (`src/index.ts`) is the public API. Internal files are grouped by domain and are not stable deep-import paths. Existing root exports remain supported; project setup also exposes `initProject`, `initConfig` and `doctor`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
This repository includes [resume.config.yaml](../resume.config.yaml); edit it to configure both examples. Leave `layout.style` commented out to keep their individual styles.
|
|
4
|
+
|
|
5
|
+
Create a YAML configuration and inspect the effective values:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx resumectl config init
|
|
9
|
+
bunx resumectl config show resume.tex --json
|
|
10
|
+
bunx resumectl build resume.tex
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A complete example of `resume.config.yaml`:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
version: 1
|
|
17
|
+
layout:
|
|
18
|
+
style: sans
|
|
19
|
+
paper: a4
|
|
20
|
+
scale: 0.86 # Sans only
|
|
21
|
+
tracking: -1.5 # Sans only, percent of an em
|
|
22
|
+
ratio: 1.125 # Sans only, type scale
|
|
23
|
+
margins:
|
|
24
|
+
top: 0.4in
|
|
25
|
+
bottom: 0.3in
|
|
26
|
+
left: 0.35in
|
|
27
|
+
right: 0.35in
|
|
28
|
+
bulletSpacing: 0.15em
|
|
29
|
+
bulletIndent: 1.5pc
|
|
30
|
+
sectionSpacing:
|
|
31
|
+
before: "*2" # Quote * multipliers in YAML; lengths such as 2ex also work
|
|
32
|
+
after: "*0.8"
|
|
33
|
+
checks:
|
|
34
|
+
bullets:
|
|
35
|
+
maxLines: 2
|
|
36
|
+
minLastLineFill: 0.8
|
|
37
|
+
maxPages: 1
|
|
38
|
+
sections:
|
|
39
|
+
WORK EXPERIENCE:
|
|
40
|
+
editable: true
|
|
41
|
+
checks:
|
|
42
|
+
maxLines: 2
|
|
43
|
+
minLastLineFill: 0.8
|
|
44
|
+
PROJECTS:
|
|
45
|
+
editable: true
|
|
46
|
+
checks:
|
|
47
|
+
maxLines: 2
|
|
48
|
+
minLastLineFill: 0.65
|
|
49
|
+
EDUCATION:
|
|
50
|
+
editable: false
|
|
51
|
+
checks:
|
|
52
|
+
maxLines: 1
|
|
53
|
+
minLastLineFill: 0
|
|
54
|
+
build:
|
|
55
|
+
outDir: ./output
|
|
56
|
+
keepLogs: false
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every field is optional; omit `layout.style` to preserve the style in each source. `scale`, `tracking` and `ratio` affect only the sans style. Default section spacing is `*1`/`*1` for serif and `*2`/`*0.8` for sans. Margins accept `pt`, `mm`, `cm`, `in` and `pc`; spacing also accepts `em` and `ex`. Values must be non-negative. The nominal body size remains 11pt.
|
|
60
|
+
|
|
61
|
+
Precedence is **CLI > YAML > documentclass > defaults**, merged field by field. Discovery starts beside the input source (or in the current directory for `config show` without a source) and searches upward to the repository root. Both `resume.config.yaml` and `resume.config.yml` are supported; if both exist, select one explicitly. Use `--config path/to/config.yaml` for another configuration. Relative `build.outDir` paths are based on the YAML file; CLI `--out-dir` paths are based on the current directory. Without an output setting, the PDF goes beside the source.
|
|
62
|
+
|
|
63
|
+
`config init` creates a starter file in the current directory and refuses to overwrite an existing file. It includes a YAML language-server schema comment pointing to [resume.config.schema.json](../resume.config.schema.json) for editor completion. Unknown keys, invalid types, unsupported versions and invalid lengths are rejected. `config show` outputs effective values plus the source of each field, without compiling.
|
|
64
|
+
|
|
65
|
+
`checks.bullets` supplies shared defaults. Each `sections.<title>.checks` overrides them field by field. `maxLines` is the maximum actual rendered lines **per entry**, not a total section budget. `minLastLineFill` is a fraction between 0 and 1: `0.8` means 80%, for either a single-line entry or the last line of a multi-line entry; `0` disables the minimum fill requirement. This is useful for facts such as skills and degrees, which should not be padded just to fill a line. `maxLines` and `maxPages` are positive integers.
|
|
66
|
+
|
|
67
|
+
Section keys match rendered titles, ignoring case, LaTeX formatting and repeated whitespace (`TALKS & AWARDS` matches `\section{TALKS \& AWARDS}`). Only listed sections are checked. Set `checks: false` on a section to skip its length checks; page count, text mappings and overfull-box checks still run. Without a sections map, the checker retains the original `WORK EXPERIENCE` default. The old `checks.section` selector and `--section` still work, narrowing checking to one section. CLI `--max-lines` and `--min-last-line-fill` override the selected sections' individual limits, or all configured sections when no selector is given.
|
|
68
|
+
|
|
69
|
+
`editable: true` tells the coding agent it may rewrite that section's body entries while preserving facts. `false` (the default) means leave them unchanged unless the user explicitly authorizes an edit. This policy is independent of checking: a section can be read-only and still fail its length limits. It is guidance exposed in `config show` and reports, not an enforced file lock or an automatic rewriting feature. Unlisted sections carry no editing permission. A coding agent should read `config show resume.tex --json`, revise the permitted entries, then build and use the per-section feedback. Changing thresholds or editing policy does not require rebuilding; changing content or layout does.
|
|
70
|
+
|
|
71
|
+
For a summary or table description, mark each checkable entry explicitly:
|
|
72
|
+
|
|
73
|
+
```latex
|
|
74
|
+
\section{SUMMARY}
|
|
75
|
+
\resumeentry{Your summary paragraph.}
|
|
76
|
+
|
|
77
|
+
\section{PROJECTS}
|
|
78
|
+
\begin{tabularx}{\textwidth}{@{} l X @{}}
|
|
79
|
+
\projectlink{https://example.com}{Project} & \resumeentry{Your project description.} \\
|
|
80
|
+
\end{tabularx}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Do not wrap list items in `\resumeentry`; they are already measured. Unwrapped prose and table text are not automatically checked. The repository example marks all six sections and checks 27 entries.
|
package/docs/quality.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Quality gates
|
|
2
|
+
|
|
3
|
+
### The checker
|
|
4
|
+
|
|
5
|
+
**Gate 1 — rendered content length.** During compilation, the class records TeX’s actual paragraph line counts, available line widths, and the positions of each paragraph’s start and end. For each configured section, the checker sums the lines in each entry and divides the final paragraph’s rendered end position minus its left edge by its available width. Both single-line and multi-line entries must meet that section's final-line fill threshold. Reports include the section, entry number, text, measured length, effective limits and AI editing policy. Overfull horizontal or vertical boxes also fail this gate, including overflow on earlier lines and in sections with length checking disabled.
|
|
6
|
+
|
|
7
|
+
Measurements are saved next to the PDF as `<name>.resumectl.json`, together with the effective layout and hashes of the source (including `\input` files) and PDF. Keep this file with the PDF for `check`; it survives log cleanup. Missing or mismatched measurements require a rebuild. Flat `itemize` and `plainlist` items are measured automatically. Wrap standalone prose and each `tabularx` X-column description in `\resumeentry{...}` to measure them, using the column's actual available width. Inline formatting, links, manual breaks and multiple paragraphs are supported; nested lists and nested entries are not. Headings, job titles, dates, locations and table labels are not measured as body entries. A configured, enabled section with no measured entries is an input error rather than a silent pass.
|
|
8
|
+
|
|
9
|
+
**Gate 2 — page count.** The checker reads the page count from the PDF itself and inflates compressed object streams to find the page tree. If that fails, it falls back to the TeX log.
|
|
10
|
+
|
|
11
|
+
**Gate 3 — copyable text.** The checker parses every ToUnicode CMap in the PDF and fails if any glyph maps to a private-use code point (U+E000–U+F8FF or the supplementary private-use planes). That is how OpenType alternates without a Unicode mapping show up. The report names the code points.
|
|
12
|
+
|
|
13
|
+
### Checker options
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
resumectl build <resume.tex> [options] compile with XeLaTeX or Tectonic, then run the gates
|
|
17
|
+
resumectl check <resume.tex> [options] run the gates on an existing PDF
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Flag | Default | Meaning |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `--config <path>` | discovered YAML | Configuration file |
|
|
23
|
+
| `--section <name>` | all configured sections; otherwise `WORK EXPERIENCE` | Check only this section |
|
|
24
|
+
| `--max-lines <n>` | per-section config, shared default `2` | Override maximum rendered lines per entry in selected sections |
|
|
25
|
+
| `--min-last-line-fill <0–1>` | per-section config, shared default `0.8` | Override minimum fraction occupied on the last line |
|
|
26
|
+
| `--max-pages <n>` | `1` | Page limit |
|
|
27
|
+
| `--style <serif\|sans>` | from YAML or class | Override the rendered font style |
|
|
28
|
+
| `--paper <a4\|letter>` | from YAML or class | Override paper size |
|
|
29
|
+
| `--pdf <path>` | PDF in the effective output directory | Existing PDF to check |
|
|
30
|
+
| `--out-dir <dir>` | from YAML, otherwise beside source | Output directory |
|
|
31
|
+
| `--keep-logs` / `--no-keep-logs` | from YAML, otherwise off | Keep or remove compiler intermediates after a passing build |
|
|
32
|
+
| `--json` | off | Report with effective config, parameter sources and per-bullet measurements |
|
|
33
|
+
|
|
34
|
+
The old `--min-chars` and `--max-width` flags are replaced by the line-count and fill controls. `check` requires a PDF built with the current CLI and its matching `.resumectl.json` file. Use the same layout config/overrides as the build; changing layout or source requires rebuilding. Checking thresholds can change without a rebuild.
|
|
35
|
+
|
|
36
|
+
Exit codes: `0` means every gate passed. `1` means a gate failed, the compile failed, or doctor found a missing dependency. `2` means a usage or input error.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Rendering and typography
|
|
2
|
+
|
|
3
|
+
### Typography
|
|
4
|
+
|
|
5
|
+
- **Serif — TeX Gyre Termes.** A free Times clone: traditional, compact, and familiar to every reader and parser. It keeps italics for job titles and taglines, and bold for names of organisations and roles.
|
|
6
|
+
- **Sans — Inter**, at its text optical size (`opsz=14`). Inter is the closest open-source match to the system UI fonts people read all day (SF Pro on macOS, which is what GitHub renders on a Mac). Three choices follow from that:
|
|
7
|
+
- **Upright only.** Sans italics look slanted and nervous at résumé sizes, so `\textit` and `\emph` render upright. Hierarchy comes from size, weight and case.
|
|
8
|
+
- **Body at 430, bold at 600.** Scaling to 0.86 thins the strokes, and 430 restores the colour of the text without reading as bold. For emphasis, 600 is used instead of Bold, which reads heavy at 9–10pt; 600 is also the weight GitHub uses for bold text.
|
|
9
|
+
- **Scaled to 0.86 with −1.5% tracking.** Inter is much wider than Times. Without the scale the same content runs two pages; the slight negative tracking matches SF Pro's tighter text setting.
|
|
10
|
+
- **Contextual alternates off.** Inter's `calt` feature swaps `( ) - +` next to capitals and digits for raised "case" glyphs. Those glyphs have no Unicode code point, so `ALEX (AJ) CHEN` or `(555) 010-0142` copied as private-use garbage. Gate 3 exists because this bug can't be seen on the page.
|
|
11
|
+
- **Static font instances.** XeTeX ignores variable-font axes and silently falls back to the default instance, which for some fonts is the lightest weight. `template/fonts/` therefore ships static 430, 500 and 600 files cut from Inter's variable font.
|
|
12
|
+
|
|
13
|
+
### Type scale (sans)
|
|
14
|
+
|
|
15
|
+
Every size sits on one modular scale, `size(n) = 10.95pt × ratio^n`, rendered at the `scale` factor. With the defaults (ratio 1.125, a "major second"; scale 0.86) that gives four sizes:
|
|
16
|
+
|
|
17
|
+
| Level | Step | Size | Weight | Tracking | Why |
|
|
18
|
+
|---|---|---|---|---|---|
|
|
19
|
+
| Name | +5 | 16.9pt | 500 | +2% | Large text reads heavier, so the weight drops; all caps need a little air |
|
|
20
|
+
| Organisation / school | +1 | 10.6pt | 600 | −1.5% | The anchor a skimming eye lands on |
|
|
21
|
+
| Body, role, dates, contact | 0 | 9.4pt | 430 / 600 | −1.5% | Dates use tabular figures so the right-hand column lines up |
|
|
22
|
+
| Section label | −1 | 8.4pt | 600 | +8% | Navigation, not content: small letter-spaced caps instead of competing on size |
|
|
23
|
+
|
|
24
|
+
Two checks keep the hierarchy honest:
|
|
25
|
+
|
|
26
|
+
- **Darkness falls monotonically down the levels.** Darkness is measured as size × stem width of `l`, which approximates how dark a line looks: name 1.82pt > organisation 1.35pt > role 1.20pt > label 1.06pt > body 0.88pt.
|
|
27
|
+
- **The body is big enough.** Inter's x-height is 0.546 em against Times' 0.45, so 9.4pt Inter has the same x-height as 11.4pt Times, within the 10–12pt that print body text usually uses.
|
|
28
|
+
|
|
29
|
+
The serif style keeps the classic sizes (`\huge` name, `\large` headings, 11pt body).
|
|
30
|
+
|
|
31
|
+
### Layout
|
|
32
|
+
|
|
33
|
+
- **Single column, no icons, no colour.** It prints cleanly in black and white, the PDF text layer reads in order, and ATS parsers have little to trip on.
|
|
34
|
+
- **Rows with dates and places on the right.** Each organisation sits in a `\datedsubsection` row with its dates flush right, followed by a `\datedline` row for the role with its location flush right.
|
|
35
|
+
- **Links look like links without looking like 2004.** Project names carry a small `↗` inside the link. There's no underline and no blue: the arrow reads as "clickable" on screen and as decoration on paper. Inter has the glyph; Termes doesn't, so the serif style draws the arrow with TikZ.
|
|
36
|
+
|
|
37
|
+
### Class options
|
|
38
|
+
|
|
39
|
+
```latex
|
|
40
|
+
\documentclass[style=sans, paper=letter, scale=0.9, tracking=-1]{resume}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Option | Values | Default | Effect |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `style` | `serif`, `sans` | `serif` | Termes with italics, or Inter upright-only |
|
|
46
|
+
| `paper` | `a4`, `letter` | `a4` | Page size; rendered line measurements follow automatically |
|
|
47
|
+
| `scale` | number | `0.86` | Sans only: Inter's size relative to the 11pt body |
|
|
48
|
+
| `tracking` | number | `-1.5` | Sans only: letter spacing in % of the em |
|
|
49
|
+
| `ratio` | number | `1.125` | Sans only: type-scale ratio between levels, e.g. `1.2` for a stronger hierarchy |
|
|
50
|
+
|
|
51
|
+
Class options remain supported. The CLI applies the effective layout after reading them, so YAML and command-line overrides change both the PDF and its measurements. Additional class options are `marginTop`, `marginBottom`, `marginLeft`, `marginRight`, `bulletSpacing`, `bulletIndent`, `sectionBefore` and `sectionAfter`.
|
|
52
|
+
|
|
53
|
+
### Commands
|
|
54
|
+
|
|
55
|
+
| Command | Use |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `\name{NAME}` | Centered header |
|
|
58
|
+
| `\basicInfo{…}` | Centered contact line; separate items with `\sep` |
|
|
59
|
+
| `\datedsubsection{left}{dates}` | Organisation or school row, with dates flush right |
|
|
60
|
+
| `\datedline{left}{right}` | Role row, with location flush right |
|
|
61
|
+
| `\role{title}` | Bold job title. Italic in serif, upright in sans |
|
|
62
|
+
| `\projectlink{url}{name}` | Bold project name with a clickable `↗` |
|
|
63
|
+
| `plainlist` environment | Bullet-less list, used for education details |
|
|
64
|
+
| `\resumeentry{body}` | Checkable prose paragraph or X-column description; no visual styling |
|
|
65
|
+
|
|
66
|
+
### Swapping a font
|
|
67
|
+
|
|
68
|
+
Custom fonts require changing `template/resume.cls` and providing the corresponding font files. The layout gate measures the compiled result, so it needs no width-table regeneration. `typographicWidth` and `bun run gen-metrics` remain available as legacy estimation utilities; they are not used by the gates.
|
package/docs/sans.png
ADDED
|
Binary file
|
package/docs/serif.png
ADDED
|
Binary file
|
package/docs/setup.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Toolchain setup
|
|
2
|
+
|
|
3
|
+
resumectl needs **Bun ≥ 1.3** and one TeX engine. When both XeLaTeX and Tectonic are installed, it uses XeLaTeX. Set `RESUMECTL_ENGINE=tectonic` or `RESUMECTL_ENGINE=xelatex` to choose one explicitly.
|
|
4
|
+
|
|
5
|
+
## Tectonic (recommended)
|
|
6
|
+
|
|
7
|
+
[Tectonic](https://tectonic-typesetting.github.io) is a single 16 MB program built on XeTeX. It downloads only the TeX packages a document needs.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
brew install tectonic # macOS
|
|
11
|
+
curl --proto '=https' --tlsv1.2 -fsSL https://drop-sh.fullyjustified.net | sh # Linux and others
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The first build downloads about 45 MB of packages and takes a couple of minutes. `build` says so on stderr. Later builds take seconds and work offline.
|
|
15
|
+
|
|
16
|
+
Tectonic ships an older LaTeX format (2021). `resume.cls` backports the two newer features it relies on, so both styles render identically to XeLaTeX. The test suite checks this whenever both engines are installed.
|
|
17
|
+
|
|
18
|
+
## XeLaTeX
|
|
19
|
+
|
|
20
|
+
Use this if you already have TeX Live or MacTeX, or prefer a full distribution. [TinyTeX-1](https://github.com/rstudio/tinytex-releases#installation) is the lightest option, at about 287 MiB with the packages below:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
tlmgr update --self
|
|
24
|
+
tlmgr install titlesec enumitem pgf tex-gyre
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
TinyTeX-1 already includes `fontspec`, `kvoptions`, `geometry`, `hyperref`, `url` and `tools` (which supplies `tabularx`). `pgf` supplies TikZ, and `tex-gyre` supplies the serif font. Inter ships with resumectl. Neither style requires installing fonts in the operating system.
|
|
28
|
+
|
|
29
|
+
## Check
|
|
30
|
+
|
|
31
|
+
Run `bunx resumectl doctor` (or `bun run resumectl doctor` in this repository). It reports a missing Bun, TeX engine, TeX package or bundled asset without installing anything. A passing doctor confirms availability; `build` is the end-to-end check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resumectl",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Clean LaTeX résumé templates and PDF quality checks for coding-agent workflows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"module": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./template/*": "./template/*",
|
|
11
|
+
"./config-schema": "./resume.config.schema.json"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"resumectl": "./src/cli.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"template",
|
|
19
|
+
"docs",
|
|
20
|
+
"GLOSSARY.md",
|
|
21
|
+
"resume.config.yaml",
|
|
22
|
+
"resume.config.schema.json",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"skills"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"resume",
|
|
29
|
+
"cv",
|
|
30
|
+
"latex",
|
|
31
|
+
"xelatex",
|
|
32
|
+
"bun",
|
|
33
|
+
"coding-agent"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"bun": ">=1.3"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"resumectl": "bun src/cli.ts",
|
|
40
|
+
"example": "bun src/cli.ts build examples/serif.tex && bun src/cli.ts build examples/sans.tex",
|
|
41
|
+
"gen-metrics": "bun scripts/gen-metrics.ts",
|
|
42
|
+
"test": "bun test",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"verify": "bun run typecheck && bun test"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.4.2",
|
|
48
|
+
"typescript": "^7.0.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "resumectl configuration",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": { "type": "string", "minLength": 1 },
|
|
8
|
+
"version": { "const": 1, "default": 1 },
|
|
9
|
+
"layout": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"style": { "enum": ["serif", "sans"], "default": "serif" },
|
|
14
|
+
"paper": { "enum": ["a4", "letter"], "default": "a4" },
|
|
15
|
+
"scale": { "type": "number", "minimum": 0.1, "maximum": 4, "default": 0.86, "description": "Sans only: font scale relative to the body size." },
|
|
16
|
+
"tracking": { "type": "number", "minimum": -10, "maximum": 20, "default": -1.5, "description": "Sans only: letter spacing in percent of an em." },
|
|
17
|
+
"ratio": { "type": "number", "minimum": 1, "maximum": 2, "default": 1.125, "description": "Sans only: type scale ratio." },
|
|
18
|
+
"margins": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"properties": {
|
|
22
|
+
"top": { "$ref": "#/$defs/absoluteLength", "default": "0.4in" },
|
|
23
|
+
"bottom": { "$ref": "#/$defs/absoluteLength", "default": "0.3in" },
|
|
24
|
+
"left": { "$ref": "#/$defs/absoluteLength", "default": "0.35in" },
|
|
25
|
+
"right": { "$ref": "#/$defs/absoluteLength", "default": "0.35in" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"bulletSpacing": { "$ref": "#/$defs/length", "default": "0.15em" },
|
|
29
|
+
"bulletIndent": { "$ref": "#/$defs/length", "default": "1.5pc" },
|
|
30
|
+
"sectionSpacing": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"description": "Space before/after section headings. Defaults: serif *1/*1; sans *2/*0.8.",
|
|
34
|
+
"properties": {
|
|
35
|
+
"before": { "$ref": "#/$defs/sectionLength" },
|
|
36
|
+
"after": { "$ref": "#/$defs/sectionLength" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"checks": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"section": { "type": "string", "minLength": 1, "description": "Optional selector: check only this section. Omit to check all configured sections; without a sections map, falls back to WORK EXPERIENCE." },
|
|
46
|
+
"maxPages": { "type": "integer", "minimum": 1, "maximum": 100, "default": 1 },
|
|
47
|
+
"bullets": { "$ref": "#/$defs/contentChecks", "description": "Shared defaults for section length checks." }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"sections": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"propertyNames": { "type": "string", "minLength": 1 },
|
|
53
|
+
"description": "Policies keyed by rendered section title; matching ignores case and repeated whitespace. Only listed sections are checked. Unlisted content is not authorized for AI edits.",
|
|
54
|
+
"additionalProperties": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"properties": {
|
|
58
|
+
"editable": { "type": "boolean", "default": false, "description": "Whether the coding agent may rewrite the section's body entries while preserving facts. Guidance only, not an enforced file lock." },
|
|
59
|
+
"checks": { "anyOf": [{ "const": false }, { "$ref": "#/$defs/contentChecks" }], "description": "Per-entry length rules, inheriting checks.bullets. Set false to skip this section's length checks; independent of editable." }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"build": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"additionalProperties": false,
|
|
66
|
+
"properties": {
|
|
67
|
+
"outDir": { "type": "string", "minLength": 1, "description": "Relative to this config file. Default: next to the source." },
|
|
68
|
+
"keepLogs": { "type": "boolean", "default": false }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"$defs": {
|
|
73
|
+
"contentChecks": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"maxLines": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Maximum actual rendered lines per entry; inherited default 2." },
|
|
78
|
+
"minLastLineFill": { "type": "number", "minimum": 0, "maximum": 1, "description": "Minimum fraction of the final line occupied by an entry, including single-line entries. Inherited default 0.8; 0 disables the minimum fill check." }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"absoluteLength": { "type": "string", "pattern": "^(?:[0-9]+(?:\\.[0-9]+)?|\\.[0-9]+)(?:pt|mm|cm|in|pc)$" },
|
|
82
|
+
"length": { "type": "string", "pattern": "^(?:[0-9]+(?:\\.[0-9]+)?|\\.[0-9]+)(?:pt|mm|cm|in|pc|em|ex)$" },
|
|
83
|
+
"sectionLength": { "anyOf": [{ "$ref": "#/$defs/length" }, { "type": "string", "pattern": "^\\*(?:[0-9]+(?:\\.[0-9]+)?|\\.[0-9]+)$" }] }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# yaml-language-server: $schema=./resume.config.schema.json
|
|
2
|
+
version: 1
|
|
3
|
+
|
|
4
|
+
layout:
|
|
5
|
+
# Uncomment to override the style in every source's documentclass.
|
|
6
|
+
# style: sans
|
|
7
|
+
paper: a4
|
|
8
|
+
scale: 0.86 # Sans only
|
|
9
|
+
tracking: -1.5 # Sans only, percent of an em
|
|
10
|
+
ratio: 1.125 # Sans only, type scale
|
|
11
|
+
margins:
|
|
12
|
+
top: 0.4in
|
|
13
|
+
bottom: 0.3in
|
|
14
|
+
left: 0.35in
|
|
15
|
+
right: 0.35in
|
|
16
|
+
bulletSpacing: 0.15em
|
|
17
|
+
bulletIndent: 1.5pc
|
|
18
|
+
# Defaults: serif *1 / *1; sans *2 / *0.8.
|
|
19
|
+
# sectionSpacing:
|
|
20
|
+
# before: "*2"
|
|
21
|
+
# after: "*0.8"
|
|
22
|
+
|
|
23
|
+
checks:
|
|
24
|
+
bullets:
|
|
25
|
+
maxLines: 2
|
|
26
|
+
minLastLineFill: 0.8 # Defaults inherited by each section below.
|
|
27
|
+
maxPages: 1
|
|
28
|
+
|
|
29
|
+
sections:
|
|
30
|
+
SUMMARY:
|
|
31
|
+
editable: true
|
|
32
|
+
checks:
|
|
33
|
+
maxLines: 3
|
|
34
|
+
minLastLineFill: 0.5
|
|
35
|
+
EDUCATION:
|
|
36
|
+
editable: false
|
|
37
|
+
checks:
|
|
38
|
+
maxLines: 1
|
|
39
|
+
minLastLineFill: 0 # Facts do not need padding to fill the line.
|
|
40
|
+
WORK EXPERIENCE:
|
|
41
|
+
editable: true
|
|
42
|
+
checks:
|
|
43
|
+
maxLines: 2
|
|
44
|
+
minLastLineFill: 0.8
|
|
45
|
+
PROJECTS:
|
|
46
|
+
editable: true
|
|
47
|
+
checks:
|
|
48
|
+
maxLines: 2
|
|
49
|
+
minLastLineFill: 0.65
|
|
50
|
+
TALKS & AWARDS:
|
|
51
|
+
editable: true
|
|
52
|
+
checks:
|
|
53
|
+
maxLines: 2
|
|
54
|
+
minLastLineFill: 0.65
|
|
55
|
+
SKILLS:
|
|
56
|
+
editable: false
|
|
57
|
+
checks:
|
|
58
|
+
maxLines: 2
|
|
59
|
+
minLastLineFill: 0
|
|
60
|
+
# Use checks: false to skip a section's length checks.
|
|
61
|
+
# editable is an instruction for the coding agent, not a file lock.
|
|
62
|
+
|
|
63
|
+
build:
|
|
64
|
+
# outDir: ./output # Omit to write beside each source file.
|
|
65
|
+
keepLogs: false
|