paperlint 2.0.0 → 2.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 (207) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +21 -0
  4. package/LICENSE +1 -1
  5. package/README.md +81 -24
  6. package/action.harness.mjs +10 -10
  7. package/action.mutations.mjs +3 -3
  8. package/action.yml +11 -11
  9. package/bin/{rpp.mjs → paperlint.mjs} +2 -2
  10. package/dist/adapters/banal/invocation.js +3 -3
  11. package/dist/adapters/banal/invocation.js.map +1 -1
  12. package/dist/adapters/banal/run.d.ts +1 -1
  13. package/dist/adapters/banal/run.js +1 -1
  14. package/dist/adapters/banal/run.js.map +1 -1
  15. package/dist/adapters/banal/settings.d.ts +4 -4
  16. package/dist/adapters/banal/settings.d.ts.map +1 -1
  17. package/dist/adapters/banal/settings.js +4 -4
  18. package/dist/adapters/banal/settings.js.map +1 -1
  19. package/dist/adapters/banal/xml.js +1 -1
  20. package/dist/adapters/banal/xml.js.map +1 -1
  21. package/dist/adapters/curl/download.io.d.ts.map +1 -1
  22. package/dist/adapters/curl/download.io.js +2 -2
  23. package/dist/adapters/curl/download.io.js.map +1 -1
  24. package/dist/build-engine.d.ts +1 -1
  25. package/dist/build-engine.d.ts.map +1 -1
  26. package/dist/build-engine.js +6 -3
  27. package/dist/build-engine.js.map +1 -1
  28. package/dist/build.d.ts +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +5 -4
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts +29 -30
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +174 -98
  35. package/dist/cli.js.map +1 -1
  36. package/dist/doctor.d.ts.map +1 -1
  37. package/dist/doctor.js +21 -6
  38. package/dist/doctor.js.map +1 -1
  39. package/dist/engine.d.ts +2 -2
  40. package/dist/engine.d.ts.map +1 -1
  41. package/dist/engine.js +1 -1
  42. package/dist/facts-file.d.ts +33 -9
  43. package/dist/facts-file.d.ts.map +1 -1
  44. package/dist/facts-file.js +117 -17
  45. package/dist/facts-file.js.map +1 -1
  46. package/dist/hooks-settings.d.ts +0 -18
  47. package/dist/hooks-settings.d.ts.map +1 -1
  48. package/dist/hooks-settings.js +70 -42
  49. package/dist/hooks-settings.js.map +1 -1
  50. package/dist/init.d.ts +13 -10
  51. package/dist/init.d.ts.map +1 -1
  52. package/dist/init.js +74 -42
  53. package/dist/init.js.map +1 -1
  54. package/dist/link-skills.js +2 -2
  55. package/dist/link-skills.js.map +1 -1
  56. package/dist/new-paper.d.ts +2 -2
  57. package/dist/new-paper.d.ts.map +1 -1
  58. package/dist/new-paper.js +17 -2
  59. package/dist/new-paper.js.map +1 -1
  60. package/dist/paper-settings.d.ts +68 -0
  61. package/dist/paper-settings.d.ts.map +1 -0
  62. package/dist/paper-settings.js +144 -0
  63. package/dist/paper-settings.js.map +1 -0
  64. package/dist/presets.d.ts +84 -0
  65. package/dist/presets.d.ts.map +1 -0
  66. package/dist/presets.js +186 -0
  67. package/dist/presets.js.map +1 -0
  68. package/dist/rules-config.d.ts +8 -1
  69. package/dist/rules-config.d.ts.map +1 -1
  70. package/dist/rules-config.js +13 -8
  71. package/dist/rules-config.js.map +1 -1
  72. package/dist/structure.d.ts.map +1 -1
  73. package/dist/structure.js +2 -1
  74. package/dist/structure.js.map +1 -1
  75. package/dist/tex-requirements.d.ts +63 -7
  76. package/dist/tex-requirements.d.ts.map +1 -1
  77. package/dist/tex-requirements.js +91 -24
  78. package/dist/tex-requirements.js.map +1 -1
  79. package/dist/toolchain.d.ts +2 -2
  80. package/dist/toolchain.d.ts.map +1 -1
  81. package/dist/toolchain.js +8 -8
  82. package/dist/toolchain.js.map +1 -1
  83. package/dist/types.d.ts +4 -4
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/venue-rules.d.ts +109 -0
  86. package/dist/venue-rules.d.ts.map +1 -0
  87. package/dist/venue-rules.js +387 -0
  88. package/dist/venue-rules.js.map +1 -0
  89. package/docs/configuration.md +73 -17
  90. package/docs/e2e.md +3 -4
  91. package/docs/install.md +11 -3
  92. package/docs/optional-rules.md +26 -25
  93. package/docs/rules.md +132 -3
  94. package/docs/toolchain.md +5 -5
  95. package/eslint-rules/paper-typography.mjs +1 -1
  96. package/eslint-rules/paper-typography.mutations.mjs +1 -1
  97. package/eslint-rules/papers.mjs +6 -2
  98. package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
  99. package/eslint-rules/pdf-last-page-balance.mjs +23 -2
  100. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  101. package/fixtures/build-e2e/guards/paper.tex +1 -1
  102. package/fixtures/pdf-facts/README.md +1 -1
  103. package/fixtures/real-markdown-paper/baseline.json +1 -1
  104. package/fixtures/real-markdown-paper/baseline.mjs +2 -2
  105. package/fixtures/toolchain-mirror/install-tl +2 -2
  106. package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
  107. package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
  108. package/hooks/hooks.harness.mjs +1 -1
  109. package/lib/paper-config.d.mts +7 -0
  110. package/lib/paper-config.harness.mjs +3 -3
  111. package/lib/paper-config.mjs +34 -3
  112. package/lib/skill-trigger-cases.harness.mjs +1 -1
  113. package/package.json +4 -4
  114. package/plugin/hooks/hooks.json +3 -3
  115. package/scripts/check.mjs +1 -1
  116. package/scripts/harness-api.frozen.json +1 -1
  117. package/scripts/harness-api.test.ts +2 -2
  118. package/scripts/layer-legacy.frozen.json +1 -1
  119. package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
  120. package/scripts/mutation-batteries.frozen.json +2 -2
  121. package/scripts/release-config.test.ts +1 -1
  122. package/skills/find-venue/SKILL.md +5 -1
  123. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  124. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  125. package/skills/paper-pipeline/SKILL.md +4 -3
  126. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  127. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  128. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  129. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
  130. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  131. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  132. package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
  133. package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
  134. package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
  135. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
  136. package/skills/render-paper/SKILL.md +3 -3
  137. package/skills/render-paper/SKILL.md.spec.ts +2 -2
  138. package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
  139. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  140. package/skills/study-accepted-papers/SKILL.md +2 -1
  141. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  142. package/skills/submit-paper/SKILL.md +9 -1
  143. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  144. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  145. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  146. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  147. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  148. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  149. package/skills/submit-paper/references/venues/venue-profile.schema.json +120 -30
  150. package/src/adapters/banal/index.test.ts +1 -1
  151. package/src/adapters/banal/invocation.test.ts +3 -3
  152. package/src/adapters/banal/invocation.ts +3 -3
  153. package/src/adapters/banal/locate.test.ts +4 -4
  154. package/src/adapters/banal/run.ts +2 -2
  155. package/src/adapters/banal/settings.test.ts +12 -6
  156. package/src/adapters/banal/settings.ts +9 -5
  157. package/src/adapters/banal/xml.test.ts +1 -1
  158. package/src/adapters/banal/xml.ts +1 -1
  159. package/src/adapters/curl/download.io.ts +4 -2
  160. package/src/adapters/curl/download.test.ts +3 -1
  161. package/src/adapters/node/files.test.ts +1 -1
  162. package/src/adapters/node/process.test.ts +2 -2
  163. package/src/adapters/node/workspace.test.ts +4 -4
  164. package/src/build-engine.harness.mjs +2 -2
  165. package/src/build-engine.ts +11 -3
  166. package/src/build.harness.mjs +27 -22
  167. package/src/build.mutations.mjs +3 -2
  168. package/src/build.ts +6 -5
  169. package/src/cli.harness.mjs +72 -131
  170. package/src/cli.mutations.mjs +7 -32
  171. package/src/cli.ts +204 -121
  172. package/src/doctor.harness.mjs +9 -25
  173. package/src/doctor.ts +28 -5
  174. package/src/engine.harness.mjs +4 -2
  175. package/src/engine.ts +2 -2
  176. package/src/facts-file.test.ts +54 -7
  177. package/src/facts-file.ts +145 -24
  178. package/src/hooks-settings.harness.mjs +24 -15
  179. package/src/hooks-settings.mutations.mjs +2 -4
  180. package/src/hooks-settings.ts +92 -49
  181. package/src/init.ts +88 -52
  182. package/src/latex-log.harness.mjs +1 -1
  183. package/src/link-skills.harness.mjs +3 -1
  184. package/src/link-skills.mutations.mjs +1 -1
  185. package/src/link-skills.ts +2 -2
  186. package/src/new-paper.harness.mjs +10 -7
  187. package/src/new-paper.test.ts +145 -0
  188. package/src/new-paper.ts +22 -2
  189. package/src/paper-settings-commands.test.ts +311 -0
  190. package/src/paper-settings.test.ts +220 -0
  191. package/src/paper-settings.ts +206 -0
  192. package/src/pdf-facts.harness.mjs +1 -1
  193. package/src/presets.test.ts +236 -0
  194. package/src/presets.ts +307 -0
  195. package/src/rules-config.ts +14 -9
  196. package/src/structure.harness.mjs +4 -4
  197. package/src/structure.ts +2 -1
  198. package/src/tex-requirements.harness.mjs +12 -18
  199. package/src/tex-requirements.ts +179 -29
  200. package/src/toolchain.harness.mjs +34 -25
  201. package/src/toolchain.ts +14 -8
  202. package/src/types.ts +4 -4
  203. package/src/venue-rules.test.ts +545 -0
  204. package/src/venue-rules.ts +573 -0
  205. package/templates/paper/paperlint.json +4 -0
  206. package/fixtures/build-e2e/acmart/venue.json +0 -1
  207. package/plugin/.claude-plugin/plugin.json +0 -8
@@ -20,12 +20,13 @@
20
20
  * allowed to know where this package is installed (rule 10).
21
21
  */
22
22
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
23
- import { existsSync, readFileSync, readdirSync } from "node:fs";
23
+ import { readFileSync, readdirSync } from "node:fs";
24
24
  import { createRequire } from "node:module";
25
25
  import { join } from "node:path";
26
26
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
27
27
  import Ajv from "ajv";
28
28
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
29
+ import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
29
30
 
30
31
  /** CTAN package name → the names that prove it is installed. */
31
32
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -78,18 +79,133 @@ function violations(file: string, v: Validate): string[] {
78
79
  );
79
80
  }
80
81
 
82
+ /** One kind of paper a venue takes (`short`, `research`, …) and its page limits; null = not limited. */
83
+ export interface KindLimits {
84
+ readonly bodyPagesMax: number | null;
85
+ readonly refPagesMax: number | null;
86
+ }
87
+
88
+ /**
89
+ * The format a venue's call for papers sets — what the `pdf/*` venue rules judge a built PDF
90
+ * against. Every field the profile leaves out is null, and a rule that finds null checks nothing:
91
+ * a profile that does not name a number never makes one up.
92
+ */
93
+ export interface VenueFormat {
94
+ readonly pageWidthIn: number | null;
95
+ readonly pageHeightIn: number | null;
96
+ readonly columns: number | null;
97
+ readonly bodyPt: number | null;
98
+ readonly bodyPtTol: number | null;
99
+ readonly refPtMin: number | null;
100
+ readonly refPtMax: number | null;
101
+ /** The prefix a font name of the body text starts with (`LinLibertine`). */
102
+ readonly fontsText: string | null;
103
+ readonly fontsTitle: string | null;
104
+ readonly kinds: ReadonlyMap<string, KindLimits>;
105
+ }
106
+
107
+ /** Every format field a preset left out is null; a merge fills it from the parent. */
108
+ export const NO_FORMAT: VenueFormat = {
109
+ pageWidthIn: null,
110
+ pageHeightIn: null,
111
+ columns: null,
112
+ bodyPt: null,
113
+ bodyPtTol: null,
114
+ refPtMin: null,
115
+ refPtMax: null,
116
+ fontsText: null,
117
+ fontsTitle: null,
118
+ kinds: new Map(),
119
+ };
120
+
121
+ /**
122
+ * One preset FILE, parsed — before its `extends` chain is resolved (`src/presets.ts` does that).
123
+ * A preset is a venue or a template family: `{ extends?, name?, template?, format?, tex?, rules? }`.
124
+ */
125
+ export interface PresetFile {
126
+ /** The preset this one builds on: `paperlint:<name>` or a relative path. */
127
+ readonly extends: string | null;
128
+ /** A display name for messages; the file name otherwise. */
129
+ readonly name: string | null;
130
+ readonly template: string | null;
131
+ /** Null when the file declares no `tex` block (allowed only with `extends`). */
132
+ readonly tex: TexRequirements | null;
133
+ readonly format: VenueFormat;
134
+ /** rule id → ESLint entry, checked against the shipped rules where a config is built. */
135
+ readonly rules: Readonly<Record<string, unknown>>;
136
+ }
137
+
138
+ type KindsJson = Readonly<
139
+ Record<string, { body_pages_max?: number; ref_pages_max?: number }>
140
+ >;
141
+
142
+ /** A preset's `format` block after the schema accepted it. */
143
+ interface FormatJson {
144
+ readonly page_w_in?: number;
145
+ readonly page_h_in?: number;
146
+ readonly columns?: number;
147
+ readonly body_pt?: number;
148
+ readonly body_pt_tol?: number;
149
+ readonly ref_pt_min?: number;
150
+ readonly ref_pt_max?: number;
151
+ readonly fonts_text?: string;
152
+ readonly fonts_title?: string;
153
+ readonly kinds?: KindsJson;
154
+ }
155
+
156
+ /** The preset's JSON after the schema accepted it — the shape `venue-profile.schema.json` allows. */
157
+ interface PresetJson {
158
+ readonly extends?: string;
159
+ readonly name?: string;
160
+ readonly template?: string;
161
+ readonly tex?: Partial<TexRequirements>;
162
+ readonly format?: FormatJson;
163
+ readonly rules?: Readonly<Record<string, unknown>>;
164
+ }
165
+
166
+ /** An optional field as the typed preset holds it: absent is null. */
167
+ const orNull = <T>(v: T | undefined): T | null => (v === undefined ? null : v);
168
+
169
+ function kindsOf(k: KindsJson | undefined): ReadonlyMap<string, KindLimits> {
170
+ return new Map(
171
+ Object.entries(k ?? {}).map(([name, v]) => [
172
+ name,
173
+ {
174
+ bodyPagesMax: orNull(v.body_pages_max),
175
+ refPagesMax: orNull(v.ref_pages_max),
176
+ },
177
+ ]),
178
+ );
179
+ }
180
+
181
+ function formatOf(j: FormatJson = {}): VenueFormat {
182
+ return {
183
+ pageWidthIn: orNull(j.page_w_in),
184
+ pageHeightIn: orNull(j.page_h_in),
185
+ columns: orNull(j.columns),
186
+ bodyPt: orNull(j.body_pt),
187
+ bodyPtTol: orNull(j.body_pt_tol),
188
+ refPtMin: orNull(j.ref_pt_min),
189
+ refPtMax: orNull(j.ref_pt_max),
190
+ fontsText: orNull(j.fonts_text),
191
+ fontsTitle: orNull(j.fonts_title),
192
+ kinds: kindsOf(j.kinds),
193
+ };
194
+ }
195
+
81
196
  /**
82
- * The text of one profile → its typed requirements, or an Error naming every problem.
197
+ * The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
198
+ * of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
83
199
  *
84
200
  * @param text the file's contents
85
201
  * @param file the name to put in messages
86
202
  * @param dir the directory holding the schema (the shipped venues directory by default)
87
203
  */
88
- export function parseProfile(
204
+ export function parsePreset(
89
205
  text: string,
90
206
  file: string,
91
207
  dir: string = packageVenuesDir(),
92
- ): TexRequirements {
208
+ ): PresetFile {
93
209
  const { config, error } = typescript().parseConfigFileTextToJson(file, text);
94
210
  if (error)
95
211
  throw new Error(
@@ -100,8 +216,38 @@ export function parseProfile(
100
216
  throw new Error(
101
217
  `${file} does not match ${SCHEMA_FILE}:\n ${violations(file, validate).join("\n ")}`,
102
218
  );
103
- const tex = (config as { tex: Partial<TexRequirements> }).tex;
104
- return { packages: tex.packages ?? {}, tools: tex.tools ?? {} };
219
+ const j = config as PresetJson;
220
+ return {
221
+ extends: orNull(j.extends),
222
+ name: orNull(j.name),
223
+ template: orNull(j.template),
224
+ tex: j.tex
225
+ ? { packages: j.tex.packages ?? {}, tools: j.tex.tools ?? {} }
226
+ : null,
227
+ format: formatOf(j.format),
228
+ rules: j.rules ?? {},
229
+ };
230
+ }
231
+
232
+ /** The text of one preset → the TeX requirements it declares itself (none without a `tex` block). */
233
+ export function parseProfile(
234
+ text: string,
235
+ file: string,
236
+ dir: string = packageVenuesDir(),
237
+ ): TexRequirements {
238
+ return parsePreset(text, file, dir).tex ?? NO_REQUIREMENTS;
239
+ }
240
+
241
+ /**
242
+ * The file a venue's profile lives in, or null when the name cannot be a venue (the base set is a
243
+ * profile file but not a venue). Whether the file exists is the caller's question.
244
+ */
245
+ export function profileFileOf(venue: string): string | null {
246
+ return venue === BASE_PROFILE.slice(0, -PROFILE_EXT.length) ||
247
+ venue.includes("/") ||
248
+ venue.includes("\\")
249
+ ? null
250
+ : `${venue}${PROFILE_EXT}`;
105
251
  }
106
252
 
107
253
  function readProfile(dir: string, file: string): TexRequirements {
@@ -141,41 +287,45 @@ export function packageNames(tex: TexRequirements): string[] {
141
287
  }
142
288
 
143
289
  /**
144
- * What ONE paper needs: the base set plus its venue's block. A paper with no venue, or a venue this
145
- * package has no profile for, gets the base set — and the source says which, so an ACM paper built
146
- * without `venue.json` is visibly running on the base set rather than silently.
290
+ * What ONE paper needs: the base set plus its venue preset's `tex` (the union over the preset's
291
+ * `extends` chain, resolved by `src/presets.ts`). A paper with no preset gets the base set, and the
292
+ * source says so, so an ACM paper built without a preset is visibly running on the base set.
147
293
  */
148
294
  export function requirementsFor(
149
- venue: string | null,
295
+ preset: { readonly label: string; readonly tex: TexRequirements } | null,
150
296
  dir: string = packageVenuesDir(),
151
297
  ): PaperRequirements {
152
298
  const base = readProfile(dir, BASE_PROFILE);
153
- if (venue === null)
154
- return { source: "the base set (no venue.json)", tex: base };
155
- const file = `${venue}${PROFILE_EXT}`;
156
- if (
157
- venue === BASE_PROFILE.slice(0, -PROFILE_EXT.length) ||
158
- !existsSync(join(dir, file))
159
- )
160
- return {
161
- source: `the base set (venue ${venue} has no profile in paperlint)`,
162
- tex: base,
163
- };
164
- return {
165
- source: `venue ${venue}`,
166
- tex: mergeRequirements(base, readProfile(dir, file)),
167
- };
299
+ return preset === null
300
+ ? {
301
+ source: `the base set (no venue preset in ${PAPER_SETTINGS_FILE})`,
302
+ tex: base,
303
+ }
304
+ : {
305
+ source: `venue ${preset.label}`,
306
+ tex: mergeRequirements(base, preset.tex),
307
+ };
168
308
  }
169
309
 
170
- /** Everything any profile declares — the set `paperlint toolchain` installs, so one tree builds any paper. */
171
- export function declaredUnion(dir: string = packageVenuesDir()): {
310
+ /**
311
+ * Everything any shipped preset declares, plus `extra` — the resolved presets of the project's own
312
+ * papers, which may live outside the package. The set `paperlint toolchain` installs, so one tree
313
+ * builds any paper.
314
+ */
315
+ export function declaredUnion(
316
+ dir: string = packageVenuesDir(),
317
+ extra: readonly TexRequirements[] = [],
318
+ ): {
172
319
  readonly tex: TexRequirements;
173
320
  readonly profiles: number;
174
321
  } {
175
322
  const venues = venueNames(dir);
176
- const tex = venues.reduce(
323
+ const shipped = venues.reduce(
177
324
  (acc, v) => mergeRequirements(acc, readProfile(dir, `${v}${PROFILE_EXT}`)),
178
325
  readProfile(dir, BASE_PROFILE),
179
326
  );
180
- return { tex, profiles: venues.length + 1 };
327
+ return {
328
+ tex: extra.reduce(mergeRequirements, shipped),
329
+ profiles: venues.length + 1,
330
+ };
181
331
  }
@@ -4,7 +4,7 @@
4
4
  * 🔴 NOT ONE BYTE OF TeX LIVE IS DOWNLOADED HERE. The mirror is a directory served by `file://`
5
5
  * URLs: the real `curl` fetches from it, the real `tar` lists and unpacks it, and the installer
6
6
  * inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
7
- * `tlmgr` answer from two text files. So what runs is rpp's own download, fallback, unpack,
7
+ * `tlmgr` answer from two text files. So what runs is paperlint's own download, fallback, unpack,
8
8
  * install and verify logic — the part that decides success — and what is faked is only TeX.
9
9
  * The real TeX Live half is `test/e2e/toolchain.mjs`.
10
10
  *
@@ -78,22 +78,25 @@ check(
78
78
  ) === '["urw-base35"]',
79
79
  );
80
80
  check(
81
- "cacheRoot: RPP_TEXLIVE_DIR wins",
82
- T.cacheRoot({ RPP_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" }, "/h") ===
83
- "/ci/tl",
81
+ "cacheRoot: PAPERLINT_TEXLIVE_DIR wins",
82
+ T.cacheRoot(
83
+ { PAPERLINT_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" },
84
+ "/h",
85
+ ) === "/ci/tl",
84
86
  );
85
87
  check(
86
88
  "cacheRoot: then XDG_CACHE_HOME",
87
- T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/rpp/texlive",
89
+ T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/paperlint/texlive",
88
90
  );
89
91
  check(
90
92
  "cacheRoot: then ~/.cache",
91
- T.cacheRoot({}, "/h") === "/h/.cache/rpp/texlive",
93
+ T.cacheRoot({}, "/h") === "/h/.cache/paperlint/texlive",
92
94
  );
93
95
  check(
94
- "mirrorsFrom: RPP_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
95
- JSON.stringify(T.mirrorsFrom({ RPP_CTAN_MIRROR: "https://m/tlnet/" })) ===
96
- '["https://m/tlnet"]',
96
+ "mirrorsFrom: PAPERLINT_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
97
+ JSON.stringify(
98
+ T.mirrorsFrom({ PAPERLINT_CTAN_MIRROR: "https://m/tlnet/" }),
99
+ ) === '["https://m/tlnet"]',
97
100
  );
98
101
  check(
99
102
  "mirrorsFrom: several defaults, every one https",
@@ -146,7 +149,9 @@ check(
146
149
  check("usableTree: an empty cache", T.usableTree([], () => true) === null);
147
150
 
148
151
  // ── the fake mirror ────────────────────────────────────────────────────────────────────
149
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-toolchain-h-")));
152
+ const work = realpathSync(
153
+ mkdtempSync(join(tmpdir(), "paperlint-toolchain-h-")),
154
+ );
150
155
  const good = join(work, "good");
151
156
  const garbage = join(work, "garbage");
152
157
  const staging = join(work, "staging", "install-tl-20260924");
@@ -259,14 +264,14 @@ const TEX = {
259
264
  tools: { texcount: ["texcount"] },
260
265
  };
261
266
  const root = join(work, "cache");
262
- // 🔴 banal goes into the work directory, never the developer's cache: without RPP_BANAL_DIR the
263
- // command installs into ~/.cache/rpp/banal, and a harness must not leave files in a home.
267
+ // 🔴 banal goes into the work directory, never the developer's cache: without PAPERLINT_BANAL_DIR the
268
+ // command installs into ~/.cache/paperlint/banal, and a harness must not leave files in a home.
264
269
  const banalDir = join(work, "banal");
265
270
  const env = {
266
271
  ...process.env,
267
- RPP_TEXLIVE_DIR: root,
268
- RPP_CTAN_MIRROR: url(good),
269
- RPP_BANAL_DIR: banalDir,
272
+ PAPERLINT_TEXLIVE_DIR: root,
273
+ PAPERLINT_CTAN_MIRROR: url(good),
274
+ PAPERLINT_BANAL_DIR: banalDir,
270
275
  };
271
276
  // A stand-in banal: a Perl script that prints what banal prints for a one-page, 10 pt probe.
272
277
  const fakeBanal = join(work, "fake-banal");
@@ -446,7 +451,7 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
446
451
  const r = cmd({
447
452
  env: {
448
453
  ...env,
449
- RPP_TEXLIVE_DIR: fresh,
454
+ PAPERLINT_TEXLIVE_DIR: fresh,
450
455
  FAKE_INSTALL_TL_FAIL: "1",
451
456
  },
452
457
  });
@@ -462,15 +467,15 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
462
467
  const r = cmd({
463
468
  env: {
464
469
  ...env,
465
- RPP_TEXLIVE_DIR: join(work, "cache-nomirror"),
466
- RPP_CTAN_MIRROR: MISSING,
470
+ PAPERLINT_TEXLIVE_DIR: join(work, "cache-nomirror"),
471
+ PAPERLINT_CTAN_MIRROR: MISSING,
467
472
  },
468
473
  });
469
474
  check(
470
475
  "no mirror answers: exit 1, names the mirrors and the override",
471
476
  r.code === 1 &&
472
477
  r.err.includes("no CTAN mirror returned install-tl") &&
473
- r.err.includes("RPP_CTAN_MIRROR"),
478
+ r.err.includes("PAPERLINT_CTAN_MIRROR"),
474
479
  r.err,
475
480
  );
476
481
  }
@@ -520,7 +525,11 @@ const withLibertine = {
520
525
  };
521
526
  const inYears = (mirror, over = {}) =>
522
527
  cmd({
523
- env: { ...env, RPP_TEXLIVE_DIR: years, RPP_CTAN_MIRROR: mirror },
528
+ env: {
529
+ ...env,
530
+ PAPERLINT_TEXLIVE_DIR: years,
531
+ PAPERLINT_CTAN_MIRROR: mirror,
532
+ },
524
533
  ...over,
525
534
  });
526
535
  check(
@@ -583,12 +592,12 @@ check(
583
592
  {
584
593
  // tlmgr says 2027, but the installer the mirrors hand out is still 2026.
585
594
  const split = join(work, "cache-split");
586
- cmd({ env: { ...env, RPP_TEXLIVE_DIR: split } });
595
+ cmd({ env: { ...env, PAPERLINT_TEXLIVE_DIR: split } });
587
596
  const r = cmd({
588
597
  env: {
589
598
  ...env,
590
- RPP_TEXLIVE_DIR: split,
591
- RPP_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
599
+ PAPERLINT_TEXLIVE_DIR: split,
600
+ PAPERLINT_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
592
601
  },
593
602
  tex: withLibertine,
594
603
  });
@@ -606,7 +615,7 @@ check(
606
615
  // ── 4. banal, the second half of the command ───────────────────────────────────────────
607
616
  {
608
617
  const dir = join(work, "banal-4");
609
- const e = { ...env, RPP_BANAL_DIR: dir };
618
+ const e = { ...env, PAPERLINT_BANAL_DIR: dir };
610
619
  const before = cmd({ env: e, check: true });
611
620
  // Guards: --check covers banal — a complete TeX Live alone must not read as a ready toolchain.
612
621
  check(
@@ -633,7 +642,7 @@ check(
633
642
  }
634
643
  {
635
644
  const r = cmd({
636
- env: { ...env, RPP_BANAL_DIR: join(work, "banal-bad") },
645
+ env: { ...env, PAPERLINT_BANAL_DIR: join(work, "banal-bad") },
637
646
  banal: { ...BANAL, sha256: "0".repeat(64) },
638
647
  });
639
648
  // Guards: the two halves are independent — a banal failure fails the command, and TeX Live still
package/src/toolchain.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `paperlint toolchain` — install upstream TeX Live, with exactly the packages the venue profiles
3
- * declare, into rpp's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
3
+ * declare, into paperlint's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
4
4
  * commit (`adapters/banal/`). `paperlint build` offers the TeX Live install on a terminal.
5
5
  *
6
6
  * 🔴 WHY paperlint INSTALLS TeX AT ALL (rule 11). "Install TeX Live yourself" was a manual step with a
@@ -19,8 +19,8 @@
19
19
  * once reported success having installed nothing: installers' exit codes are not evidence.)
20
20
  * A second run with everything present does nothing and says so; `--check` only reports.
21
21
  *
22
- * Where: `$RPP_TEXLIVE_DIR` when set (CI points its cache there), else
23
- * `$XDG_CACHE_HOME/rpp/texlive`, else `~/.cache/rpp/texlive` — one tree per TeX Live year inside.
22
+ * Where: `$PAPERLINT_TEXLIVE_DIR` when set (CI points its cache there), else
23
+ * `$XDG_CACHE_HOME/paperlint/texlive`, else `~/.cache/paperlint/texlive` — one tree per TeX Live year inside.
24
24
  * Processes run through the injected `run` (the port `build.ts` uses), so the harness drives the
25
25
  * real download/unpack/verify logic against a fake mirror on disk, never the network.
26
26
  */
@@ -56,8 +56,8 @@ import {
56
56
  type TexRequirements,
57
57
  } from "./tex-requirements.ts";
58
58
 
59
- export const CACHE_ENV = "RPP_TEXLIVE_DIR";
60
- export const MIRROR_ENV = "RPP_CTAN_MIRROR";
59
+ export const CACHE_ENV = "PAPERLINT_TEXLIVE_DIR";
60
+ export const MIRROR_ENV = "PAPERLINT_CTAN_MIRROR";
61
61
 
62
62
  /**
63
63
  * The CTAN mirrors, redirector first. 🔴 SEVERAL, AND THAT IS A MEASUREMENT: run 33650803245 died
@@ -99,7 +99,11 @@ export function cacheRoot(
99
99
  home: string = homedir(),
100
100
  ): string {
101
101
  if (env[CACHE_ENV]) return env[CACHE_ENV];
102
- return join(env["XDG_CACHE_HOME"] || join(home, ".cache"), "rpp", "texlive");
102
+ return join(
103
+ env["XDG_CACHE_HOME"] || join(home, ".cache"),
104
+ "paperlint",
105
+ "texlive",
106
+ );
103
107
  }
104
108
 
105
109
  /**
@@ -353,7 +357,9 @@ export function installBase(
353
357
  mirrors: readonly string[],
354
358
  newerThan: string | null = null,
355
359
  ): Step<{ tree: CachedTree; mirror: string }> {
356
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-install-tl-")));
360
+ const work = realpathSync(
361
+ mkdtempSync(join(tmpdir(), "paperlint-install-tl-")),
362
+ );
357
363
  try {
358
364
  io.log(` downloading install-tl…`);
359
365
  const mirror = downloadInstaller(
@@ -409,7 +415,7 @@ function runInstallTl(
409
415
  ): Step<{ tree: CachedTree; mirror: string }> {
410
416
  const dir = join(a.root, a.year);
411
417
  mkdirSync(dir, { recursive: true });
412
- const profile = join(a.work, "rpp.profile");
418
+ const profile = join(a.work, "paperlint.profile");
413
419
  writeFileSync(profile, tlProfile(dir));
414
420
  io.log(
415
421
  ` install-tl: TeX Live ${a.year} scheme-basic from ${a.mirror} (~150 MB, about a minute)…`,
package/src/types.ts CHANGED
@@ -5,8 +5,8 @@ import type { RuleBlock } from "./rules-config.ts";
5
5
  * in someone's head: a typo in a config key read as "field not set", not as an error.
6
6
  */
7
7
 
8
- /** The contents of the consumer's `rpp.json`. */
9
- export interface RppConfig {
8
+ /** The consumer's settings — the `paperlint` key of its `package.json`. */
9
+ export interface PaperlintConfig {
10
10
  /** 🔴 REQUIRED. The paper director(ies), relative to the config file ITSELF. The field name is
11
11
  * `PAPERS_DIR_FIELD` in lib/paper-config.mjs; code reads it through `papersDirOf()` in cli.ts. */
12
12
  papersDir?: string | string[];
@@ -26,7 +26,7 @@ export interface RppConfig {
26
26
  structure?: StructureConfig | false;
27
27
  /** REMOVED — `paperlint build` compiles the paper itself. Still typed so a leftover key can be named. */
28
28
  buildScripts?: unknown;
29
- /** ESLint config blocks appended after rpp's own — PARSED by `parseSettings` in cli.ts. */
29
+ /** ESLint config blocks appended after paperlint's own — PARSED by `parseSettings` in cli.ts. */
30
30
  rules?: readonly RuleBlock[];
31
31
  }
32
32
 
@@ -102,5 +102,5 @@ export interface BuildResult {
102
102
 
103
103
  /** The result of reading the config: either data, or the exit code the caller exits with. */
104
104
  export type ConfigRead =
105
- | { opts: RppConfig; configPath: string | null; code?: undefined }
105
+ | { opts: PaperlintConfig; configPath: string | null; code?: undefined }
106
106
  | { code: number; opts?: undefined; configPath?: undefined };