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.
Files changed (41) hide show
  1. package/GLOSSARY.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +63 -2
  4. package/docs/adr/0001-domain-first-organization.md +32 -0
  5. package/docs/agents/domain.md +12 -0
  6. package/docs/agents/issue-tracker.md +10 -0
  7. package/docs/agents/triage-labels.md +11 -0
  8. package/docs/api.md +16 -0
  9. package/docs/configuration.md +83 -0
  10. package/docs/quality.md +36 -0
  11. package/docs/rendering.md +68 -0
  12. package/docs/sans.png +0 -0
  13. package/docs/serif.png +0 -0
  14. package/docs/setup.md +31 -0
  15. package/package.json +48 -4
  16. package/resume.config.schema.json +85 -0
  17. package/resume.config.yaml +65 -0
  18. package/skills/resumectl/SKILL.md +143 -0
  19. package/src/cli/main.ts +108 -0
  20. package/src/cli.ts +14 -0
  21. package/src/content/tex.ts +107 -0
  22. package/src/errors.ts +3 -0
  23. package/src/index.ts +22 -0
  24. package/src/project/config.ts +265 -0
  25. package/src/project/doctor.ts +76 -0
  26. package/src/project/init.ts +78 -0
  27. package/src/quality/gates.ts +113 -0
  28. package/src/quality/pdf.ts +84 -0
  29. package/src/rendering/assets.ts +4 -0
  30. package/src/rendering/compile.ts +101 -0
  31. package/src/rendering/font-metrics.ts +89 -0
  32. package/src/rendering/font-width.ts +11 -0
  33. package/src/rendering/measurements.ts +81 -0
  34. package/src/rendering/metrics/sans.json +122 -0
  35. package/src/rendering/metrics/serif.json +121 -0
  36. package/template/fonts/Inter-430.ttf +0 -0
  37. package/template/fonts/Inter-500.ttf +0 -0
  38. package/template/fonts/Inter-600.ttf +0 -0
  39. package/template/fonts/OFL-Inter.txt +93 -0
  40. package/template/resume-measure.tex +74 -0
  41. package/template/resume.cls +203 -0
@@ -0,0 +1,74 @@
1
+ % Loaded by resume.cls only for CLI builds. Zero-width position records are
2
+ % written at shipout; paragraph line counts come from TeX's actual line breaker.
3
+ % Paragraph hooks: https://www.latex-project.org/help/documentation/ltpara-doc.pdf
4
+ \newwrite\resume@measurestream
5
+ \immediate\openout\resume@measurestream=\jobname.rmlayout
6
+ \newcount\resume@itemid
7
+ \newcount\resume@paragraphid
8
+ \newif\ifresume@entry
9
+ \def\resume@beginentry{%
10
+ \global\advance\resume@itemid by1
11
+ \global\resume@paragraphid=0
12
+ \immediate\write\resume@measurestream{B|\the\resume@itemid|\expandafter\detokenize\expandafter{\resume@sectiontitle}}%
13
+ }
14
+ \RenewDocumentCommand\resumeentry{+m}{%
15
+ \par
16
+ \begingroup
17
+ \ifresume@entry\PackageError{resume}{Do not nest resumeentry}{Use one resumeentry per paragraph or table description.}\fi
18
+ \ifnum\@listdepth>0\PackageError{resume}{Do not use resumeentry inside a list}{List items are measured automatically.}\fi
19
+ \resume@entrytrue\resume@beginentry
20
+ #1\par
21
+ \endgroup
22
+ }
23
+ \def\resume@ifmeasuring#1{%
24
+ \ifresume@entry #1\else
25
+ \ifnum\@itemdepth=1\ifnum\@listdepth=1 #1\fi\fi
26
+ \fi
27
+ }
28
+ \def\resume@sectiontitle{}
29
+ \NewCommandCopy\resume@originalsection\section
30
+ \RenewDocumentCommand\section{s o m}{%
31
+ \par
32
+ \gdef\resume@sectiontitle{#3}%
33
+ \IfBooleanTF{#1}{\resume@originalsection*{#3}}{%
34
+ \IfNoValueTF{#2}{\resume@originalsection{#3}}{\resume@originalsection[#2]{#3}}}%
35
+ }
36
+ \NewCommandCopy\resume@originalitem\item
37
+ \RenewDocumentCommand\item{o}{%
38
+ \IfNoValueTF{#1}{\resume@originalitem}{\resume@originalitem[#1]}%
39
+ \ifnum\@itemdepth=1\ifnum\@listdepth=1
40
+ \resume@beginentry
41
+ \fi\fi
42
+ }
43
+ \def\resume@position#1{%
44
+ \pdfsavepos
45
+ % Freeze the IDs now, but expand positions only when this node ships out.
46
+ \edef\resume@writeposition{\noexpand\write\resume@measurestream{#1|\the\resume@itemid|\the\resume@paragraphid|\noexpand\the\noexpand\pdflastxpos|\noexpand\the\noexpand\pdflastypos}}%
47
+ \resume@writeposition
48
+ }
49
+ \AddToHook{para/begin}{%
50
+ \resume@ifmeasuring{%
51
+ \global\advance\resume@paragraphid by1
52
+ \immediate\write\resume@measurestream{P|\the\resume@itemid|\the\resume@paragraphid|\number\linewidth}%
53
+ \resume@position{S}%
54
+ }%
55
+ }
56
+ \AddToHook{para/end}{%
57
+ \resume@ifmeasuring{\resume@position{E}}%
58
+ }
59
+ \AddToHook{para/after}{%
60
+ \resume@ifmeasuring{%
61
+ % \prevgraf must be read outside \write's expansion context (where it is 0).
62
+ \edef\resume@lines{\the\prevgraf}%
63
+ \immediate\write\resume@measurestream{L|\the\resume@itemid|\the\resume@paragraphid|\resume@lines}%
64
+ }%
65
+ }
66
+ % Nested lists aren't flat resume bullets; fail clearly rather than miscount them.
67
+ \def\resume@nestedlist{%
68
+ \ifresume@entry\PackageError{resume}{Do not nest lists inside resumeentry}{Use a flat list without resumeentry.}\fi
69
+ \ifnum\@itemdepth>0\immediate\write\resume@measurestream{U|\the\resume@itemid}\fi
70
+ }
71
+ \AddToHook{env/itemize/begin}{\resume@nestedlist}
72
+ \AddToHook{env/enumerate/begin}{\resume@nestedlist}
73
+ % enumitem lowers its depth before its final \par; finish while still in scope.
74
+ \AddToHook{env/itemize/end}{\par}
@@ -0,0 +1,203 @@
1
+ % resume.cls — a one-page résumé class with a serif and a sans-serif style.
2
+ % Compile with XeLaTeX. MIT License.
3
+ %
4
+ % \documentclass{resume} % serif (TeX Gyre Termes)
5
+ % \documentclass[style=sans]{resume} % sans (Inter), upright only
6
+ % \documentclass[style=sans, paper=letter, scale=0.9, tracking=-1]{resume}
7
+ %
8
+ % Options (key=value):
9
+ % style = serif | sans default serif
10
+ % paper = a4 | letter default a4
11
+ % scale = <number> sans only: font scale vs. the 11pt body, default 0.86
12
+ % tracking = <number> sans only: letter spacing in % of the em, default -1.5
13
+ % ratio = <number> sans only: type-scale ratio between levels, default 1.125
14
+ %
15
+ % CLI builds apply resolved config after class options and measure actual bullets.
16
+ \NeedsTeXFormat{LaTeX2e}
17
+ \ProvidesClass{resume}[2026/10/03 v1.0 One-page resume, serif and sans styles]
18
+
19
+ \RequirePackage{kvoptions}
20
+ \SetupKeyvalOptions{family=resume, prefix=resume@}
21
+ \DeclareStringOption[serif]{style}
22
+ \DeclareStringOption[a4]{paper}
23
+ \DeclareStringOption[0.86]{scale}
24
+ \DeclareStringOption[-1.5]{tracking}
25
+ \DeclareStringOption[1.125]{ratio}
26
+ \DeclareStringOption[0.4in]{marginTop}
27
+ \DeclareStringOption[0.3in]{marginBottom}
28
+ \DeclareStringOption[0.35in]{marginLeft}
29
+ \DeclareStringOption[0.35in]{marginRight}
30
+ \DeclareStringOption[0.15em]{bulletSpacing}
31
+ \DeclareStringOption[1.5pc]{bulletIndent}
32
+ \DeclareStringOption[]{sectionBefore}
33
+ \DeclareStringOption[]{sectionAfter}
34
+ \ProcessKeyvalOptions*
35
+ \ifdefined\ResumeConfigOptions
36
+ \edef\resume@applyconfig{\noexpand\setkeys{resume}{\ResumeConfigOptions}}%
37
+ \resume@applyconfig
38
+ \fi
39
+
40
+ \LoadClass[11pt]{article}
41
+
42
+ \setlength{\parindent}{0pt}
43
+ \pagenumbering{gobble}
44
+
45
+ \RequirePackage[colorlinks=false, hidelinks]{hyperref}
46
+ \RequirePackage{url}
47
+ \urlstyle{same}
48
+ \RequirePackage{tikz}
49
+ \RequirePackage{tabularx}
50
+
51
+ % ---------------------------------------------------------------- fonts
52
+ \RequirePackage{fontspec}
53
+ % Tectonic ships a 2021 LaTeX format. Backport what newer formats provide:
54
+ % \fpeval (kernel since 2022-06), and array's \@finalstrut, which backs up a
55
+ % line when a p-cell ends in vertical mode instead of adding an empty one.
56
+ \IfFormatAtLeastTF{2022-06-01}{}{%
57
+ \RequirePackage{xfp}%
58
+ \def\@finalstrut#1{%
59
+ \unskip
60
+ \ifhmode\nobreak\else\ifnum\lastnodetype>\m@ne\vskip-\baselineskip\fi\fi
61
+ \vrule\@width\z@\@height\z@\@depth\dp#1}%
62
+ }
63
+ \def\resume@sans{sans}
64
+ \newif\ifresume@sansstyle
65
+ \ifx\resume@style\resume@sans\resume@sansstyletrue\fi
66
+ \ifx\resume@sectionBefore\@empty
67
+ \ifresume@sansstyle\def\resume@sectionBefore{*2}\else\def\resume@sectionBefore{*1}\fi
68
+ \fi
69
+ \ifx\resume@sectionAfter\@empty
70
+ \ifresume@sansstyle\def\resume@sectionAfter{*0.8}\else\def\resume@sectionAfter{*1}\fi
71
+ \fi
72
+ \ifresume@sansstyle
73
+ % Inter at text optical size, cut into static instances because XeTeX
74
+ % ignores variable-font axes. Sans style is upright only: \textit and
75
+ % \emph render in the body face. Weights: body 430 (offsets the thinner
76
+ % strokes from Scale=0.86), bold 600, name 500.
77
+ % Font directory: \ResumeFontDir if defined (`resumectl build` passes an
78
+ % absolute path), else ./fonts/ when compiling next to the class.
79
+ \ifdefined\ResumeFontDir
80
+ \defaultfontfeatures[Inter]{Path=\ResumeFontDir}
81
+ \else
82
+ \defaultfontfeatures[Inter]{Path=fonts/}
83
+ \fi
84
+ \setmainfont{Inter}[
85
+ Extension=.ttf,
86
+ Scale=\resume@scale,
87
+ LetterSpace=\resume@tracking,
88
+ % calt would swap ( ) - + next to capitals and digits for case-sensitive
89
+ % glyphs that have no Unicode mapping: they copy out of the PDF as
90
+ % private-use characters and break ATS parsing of e.g. phone numbers.
91
+ Contextuals=NoAlternate,
92
+ UprightFont=*-430,
93
+ BoldFont=*-600,
94
+ ItalicFont=*-430,
95
+ BoldItalicFont=*-600,
96
+ ]
97
+ % Type scale: size(n) = 10.95pt x ratio^n, rendered at x scale, so n=0 is the body.
98
+ % n=+5 name · n=+1 organisation · n=0 body, role, dates, contact · n=-1 section label
99
+ % Stroke darkness (size x stem width) then falls monotonically down the levels:
100
+ % name 500 > organisation 600 > role 600 > section label 600 > body 430.
101
+ \newcommand{\typestep}[1]{%
102
+ \fontsize{\fpeval{round(10.95*(\resume@ratio)^(#1),2)}}%
103
+ {\fpeval{round(10.95*(\resume@ratio)^(#1)*1.2,2)}}\selectfont}
104
+ % Large sizes read heavier, so the name drops to 500; all caps get +2% tracking.
105
+ \newfontfamily\resume@namefont{Inter}[Extension=.ttf, Scale=\resume@scale, LetterSpace=2,
106
+ Contextuals=NoAlternate, UprightFont=*-500, BoldFont=*-500]
107
+ % Small all-caps labels need open tracking (+8%) and SemiBold to hold their level.
108
+ \newfontfamily\resume@labelfont{Inter}[Extension=.ttf, Scale=\resume@scale, LetterSpace=8,
109
+ Contextuals=NoAlternate, UprightFont=*-600, BoldFont=*-600]
110
+ % Tabular figures so the right-hand date column lines up digit for digit.
111
+ \newcommand{\resume@dates}{\normalsize\addfontfeatures{Numbers=Monospaced}}
112
+ % Inter carries U+2197; the arrow sits in the body weight next to a bold name.
113
+ \newcommand{\resume@arrow}{\textmd{↗}}
114
+ \else
115
+ % Resolve files from the active TeX distribution, so a host-installed font
116
+ % with the same family name cannot silently supply the serif face.
117
+ \setmainfont{texgyretermes}[
118
+ Extension=.otf,
119
+ UprightFont=*-regular,
120
+ BoldFont=*-bold,
121
+ ItalicFont=*-italic,
122
+ BoldItalicFont=*-bolditalic,
123
+ ]
124
+ \newcommand{\resume@dates}{}
125
+ % Termes has no U+2197, so draw the arrow.
126
+ \newcommand{\resume@arrow}{\tikz[baseline=-0.1ex]\draw[->, line width=0.5pt] (0,0) -- (0.5em,0.5em);}
127
+ \fi
128
+
129
+ % ---------------------------------------------------------------- page
130
+ \def\resume@letter{letter}
131
+ \ifx\resume@paper\resume@letter
132
+ \PassOptionsToPackage{letterpaper}{geometry}
133
+ \else
134
+ \PassOptionsToPackage{a4paper}{geometry}
135
+ \fi
136
+ \RequirePackage[
137
+ left=\resume@marginLeft,
138
+ right=\resume@marginRight,
139
+ top=\resume@marginTop,
140
+ bottom=\resume@marginBottom,
141
+ nohead
142
+ ]{geometry}
143
+
144
+ % ---------------------------------------------------------------- lists
145
+ \RequirePackage{enumitem}
146
+ \setlist{noitemsep}
147
+ \setlist[itemize]{topsep=0.25em, itemsep=\resume@bulletSpacing, leftmargin=\resume@bulletIndent, font=\scriptsize}
148
+
149
+ % ---------------------------------------------------------------- sections
150
+ \RequirePackage{titlesec}
151
+ \ifresume@sansstyle
152
+ % Section labels: small letter-spaced caps (step -1); being small, they need
153
+ % more room above to separate sections. Organisations step up to +1.
154
+ \titleformat{\section}{\resume@labelfont\typestep{-1}\raggedright}{}{0em}{}[\titlerule]
155
+ \titleformat{\subsection}{\typestep{1}\raggedright}{}{0em}{}
156
+ \else
157
+ \titleformat{\section}{\large\raggedright}{}{0em}{}[\titlerule]
158
+ \titleformat{\subsection}{\large\raggedright}{}{0em}{}
159
+ \fi
160
+ \titlespacing*{\section}{0cm}{\resume@sectionBefore}{\resume@sectionAfter}
161
+ \titlespacing*{\subsection}{0cm}{*1.2}{*0.6}
162
+
163
+ % ---------------------------------------------------------------- commands
164
+ % \name{<NAME>} centered header
165
+ \newcommand{\name}[1]{%
166
+ \ifresume@sansstyle
167
+ \centerline{\resume@namefont\typestep{5}#1}%
168
+ \else
169
+ \centerline{\huge{#1}}%
170
+ \fi
171
+ \vspace{1.25ex}%
172
+ }
173
+ % \basicInfo{<contact line>} centered contact line under the name
174
+ \newcommand{\basicInfo}[1]{%
175
+ \ifresume@sansstyle
176
+ \centerline{#1}% supporting information: body size
177
+ \else
178
+ \centerline{\large{#1}}%
179
+ \fi
180
+ \vspace{1.5ex}%
181
+ }
182
+ % \sep middle dot between contact items
183
+ \newcommand{\sep}{\ \textperiodcentered\ }
184
+ % \datedsubsection{<left>}{<dates>} organisation / school row
185
+ \newcommand{\datedsubsection}[2]{%
186
+ \subsection[#1]{#1 \hfill {\resume@dates #2}}%
187
+ }
188
+ % \datedline{<left>}{<right>} role / location row
189
+ \newcommand{\datedline}[2]{%
190
+ {\par #1 \hfill #2 \par}%
191
+ }
192
+ % \role{<title>} job title; italic in serif, upright in sans
193
+ \newcommand{\role}[1]{\textbf{\textit{#1}}}
194
+ % \projectlink{<url>}{<name>} bold project name with a clickable ↗
195
+ \newcommand{\projectlink}[2]{\textbf{\href{#1}{#2\,\resume@arrow}}}
196
+ % plainlist label-less list for education details
197
+ \newenvironment{plainlist}
198
+ {\begin{itemize}[label={}, labelsep=0pt, leftmargin=0pc]}
199
+ {\end{itemize}}
200
+ % Mark one checkable prose paragraph or X-column description. No visual styling.
201
+ \NewDocumentCommand\resumeentry{+m}{\par #1\par}
202
+
203
+ \ifdefined\ResumeMeasure\input{resume-measure.tex}\fi