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
@@ -1,43 +1,121 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://github.com/zernie/research-paper-pipeline/venue-profile.schema.json",
4
- "title": "Venue profile",
5
- "description": "One venue's format profile (venues/<venue>.jsonc) and the TeX base set (venues/tex-base.jsonc). The format numbers are read by the consumer's pdf/profile rule; `tex` is read by `paperlint toolchain` and `paperlint build`.",
3
+ "$id": "https://github.com/zernie/paperlint/venue-profile.schema.json",
4
+ "title": "Venue preset",
5
+ "description": "One venue preset or template family (venues/<name>.jsonc), and the TeX base set (venues/tex-base.jsonc). A paper's paperlint.json names one with \"extends\"; a preset may extend another. `format` and `rules` are read by paperlint's pdf/* venue rules and its lint config (src/venue-rules.ts, src/presets.ts); `tex` by `paperlint toolchain` and `paperlint build`.",
6
6
  "type": "object",
7
- "required": ["tex"],
8
7
  "additionalProperties": false,
9
8
  "properties": {
10
- "tex": { "$ref": "#/definitions/tex" },
9
+ "extends": {
10
+ "description": "The preset this one builds on: `paperlint:<name>` (shipped) or a path starting with ./ or ../, relative to this file.",
11
+ "type": "string",
12
+ "minLength": 1
13
+ },
14
+ "name": {
15
+ "description": "A display name for messages; the file name otherwise.",
16
+ "type": "string",
17
+ "minLength": 1
18
+ },
19
+ "tex": {
20
+ "$ref": "#/definitions/tex"
21
+ },
11
22
  "template": {
12
23
  "description": "The \\documentclass the venue's template uses.",
13
24
  "type": "string",
14
25
  "minLength": 1
15
26
  },
16
- "page_size": { "type": "string", "enum": ["letter", "a4"] },
17
- "page_w_in": { "type": "number", "exclusiveMinimum": 0 },
18
- "page_h_in": { "type": "number", "exclusiveMinimum": 0 },
19
- "textblock_w_in": { "type": "number", "exclusiveMinimum": 0 },
20
- "textblock_h_in": { "type": "number", "exclusiveMinimum": 0 },
21
- "columns": { "type": "integer", "minimum": 1 },
22
- "body_pt": { "type": "number", "exclusiveMinimum": 0 },
23
- "body_pt_tol": { "type": "number", "minimum": 0 },
24
- "ref_pt_min": { "type": "number", "exclusiveMinimum": 0 },
25
- "ref_pt_max": { "type": "number", "exclusiveMinimum": 0 },
26
- "fonts_text": { "type": "string", "minLength": 1 },
27
- "fonts_title": { "type": "string", "minLength": 1 },
28
- "page_limits_not_gated": { "type": "object" },
29
- "kinds": {
27
+ "format": {
28
+ "description": "What the venue's call for papers sets. Every key is optional; a child preset's key replaces its parent's, and a child's kind replaces that kind.",
30
29
  "type": "object",
31
- "additionalProperties": {
32
- "type": "object",
33
- "additionalProperties": false,
34
- "properties": {
35
- "body_pages_max": { "type": "integer", "minimum": 1 },
36
- "ref_pages_max": { "type": "integer", "minimum": 0 }
30
+ "additionalProperties": false,
31
+ "properties": {
32
+ "page_size": {
33
+ "type": "string",
34
+ "enum": ["letter", "a4"]
35
+ },
36
+ "page_w_in": {
37
+ "type": "number",
38
+ "exclusiveMinimum": 0
39
+ },
40
+ "page_h_in": {
41
+ "type": "number",
42
+ "exclusiveMinimum": 0
43
+ },
44
+ "textblock_w_in": {
45
+ "type": "number",
46
+ "exclusiveMinimum": 0
47
+ },
48
+ "textblock_h_in": {
49
+ "type": "number",
50
+ "exclusiveMinimum": 0
51
+ },
52
+ "columns": {
53
+ "type": "integer",
54
+ "minimum": 1
55
+ },
56
+ "body_pt": {
57
+ "type": "number",
58
+ "exclusiveMinimum": 0
59
+ },
60
+ "body_pt_tol": {
61
+ "type": "number",
62
+ "minimum": 0
63
+ },
64
+ "ref_pt_min": {
65
+ "type": "number",
66
+ "exclusiveMinimum": 0
67
+ },
68
+ "ref_pt_max": {
69
+ "type": "number",
70
+ "exclusiveMinimum": 0
71
+ },
72
+ "fonts_text": {
73
+ "type": "string",
74
+ "minLength": 1
75
+ },
76
+ "fonts_title": {
77
+ "type": "string",
78
+ "minLength": 1
79
+ },
80
+ "page_limits_not_gated": {
81
+ "type": "object"
82
+ },
83
+ "kinds": {
84
+ "type": "object",
85
+ "additionalProperties": {
86
+ "type": "object",
87
+ "additionalProperties": false,
88
+ "properties": {
89
+ "body_pages_max": {
90
+ "type": "integer",
91
+ "minimum": 1
92
+ },
93
+ "ref_pages_max": {
94
+ "type": "integer",
95
+ "minimum": 0
96
+ }
97
+ }
98
+ }
37
99
  }
38
100
  }
101
+ },
102
+ "rules": {
103
+ "description": "Rules the venue implies, rule id \u2192 ESLint entry (a severity, or [severity, options]). Applied to every paper that extends this preset, before the paper's own rules.",
104
+ "type": "object"
39
105
  }
40
106
  },
107
+ "allOf": [
108
+ {
109
+ "if": {
110
+ "not": {
111
+ "required": ["extends"]
112
+ }
113
+ },
114
+ "then": {
115
+ "required": ["tex"]
116
+ }
117
+ }
118
+ ],
41
119
  "definitions": {
42
120
  "ctanName": {
43
121
  "description": "A package name as tlmgr knows it (CTAN / TeX Live), not a distribution package name.",
@@ -48,7 +126,11 @@
48
126
  "type": "array",
49
127
  "minItems": 1,
50
128
  "uniqueItems": true,
51
- "items": { "type": "string", "minLength": 1, "pattern": "^[^/\\s]+$" }
129
+ "items": {
130
+ "type": "string",
131
+ "minLength": 1,
132
+ "pattern": "^[^/\\s]+$"
133
+ }
52
134
  },
53
135
  "tex": {
54
136
  "description": "The TeX Live packages this template needs. Each package maps to the files that PROVE it is present: `kpsewhich` must find every one after an install, or the install failed and says which package.",
@@ -59,14 +141,22 @@
59
141
  "packages": {
60
142
  "type": "object",
61
143
  "minProperties": 1,
62
- "propertyNames": { "$ref": "#/definitions/ctanName" },
63
- "additionalProperties": { "$ref": "#/definitions/proofs" }
144
+ "propertyNames": {
145
+ "$ref": "#/definitions/ctanName"
146
+ },
147
+ "additionalProperties": {
148
+ "$ref": "#/definitions/proofs"
149
+ }
64
150
  },
65
151
  "tools": {
66
152
  "description": "Packages that ship a PROGRAM rather than a file LaTeX loads. Proved by an executable of that name in TeX Live's bin directory, not by kpsewhich.",
67
153
  "type": "object",
68
- "propertyNames": { "$ref": "#/definitions/ctanName" },
69
- "additionalProperties": { "$ref": "#/definitions/proofs" }
154
+ "propertyNames": {
155
+ "$ref": "#/definitions/ctanName"
156
+ },
157
+ "additionalProperties": {
158
+ "$ref": "#/definitions/proofs"
159
+ }
70
160
  }
71
161
  }
72
162
  }
@@ -54,7 +54,7 @@ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry
54
54
  [2, 1],
55
55
  );
56
56
  const c = run.calls[0];
57
- // Guards: the poppler-free path — banal is handed rpp's XML, never the PDF.
57
+ // Guards: the poppler-free path — banal is handed paperlint's XML, never the PDF.
58
58
  assert.match(c?.args.at(-1) ?? "", /\.xml$/);
59
59
  assert.match(c?.env["PDFTOHTML"] ?? "", /^'.*pdftohtml'$/);
60
60
  });
@@ -22,7 +22,7 @@ import {
22
22
  import type { LocatedBanal } from "./locate.ts";
23
23
  import { XML_DIALECT } from "./xml.ts";
24
24
 
25
- test("the staging holds BOTH files: the XML and the -v stub answering the dialect rpp writes", () => {
25
+ test("the staging holds BOTH files: the XML and the -v stub answering the dialect paperlint writes", () => {
26
26
  const [xml, stub] = stageBanalInput([]).files;
27
27
  assert.equal(xml.name, "paper.xml");
28
28
  assert.match(xml.content, /^<\?xml/);
@@ -50,14 +50,14 @@ test("shQuote survives a single quote", () => {
50
50
  assert.equal(shQuote("it's"), `'it'"'"'s'`);
51
51
  });
52
52
 
53
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-stage-test-")));
53
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
54
54
  after(() => rmSync(root, { recursive: true, force: true }));
55
55
 
56
56
  test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
57
57
  const awkward = join(root, "it's a dir");
58
58
  mkdirSync(awkward);
59
59
  const { workspace } = nodeAdapters({ tmpDir: awkward });
60
- workspace.within("rpp-banal-", (s) => {
60
+ workspace.within("paperlint-banal-", (s) => {
61
61
  const staged = stage(s, stageBanalInput([]));
62
62
  assert.ok(existsSync(staged.xml));
63
63
  // Guards: banal runs `$PDFTOHTML -v 2>&1 |` through /bin/sh, unquoted.
@@ -30,13 +30,13 @@ export const shQuote = (s: string): ShQuoted =>
30
30
  /** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
31
31
  export const PDFTOHTML_STUB = [
32
32
  "#!/bin/sh",
33
- "# research-paper-pipeline: banal reads the banal input XML rpp writes, never a PDF. banal still",
34
- "# asks `pdftohtml -v` which dialect to expect; this answers with the one rpp writes.",
33
+ "# paperlint: banal reads the banal input XML it writes, never a PDF. banal still",
34
+ "# asks `pdftohtml -v` which dialect to expect; this answers with the one paperlint writes.",
35
35
  'if [ "$1" = "-v" ]; then',
36
36
  ` echo "pdftohtml version ${XML_DIALECT.version}"`,
37
37
  " exit 0",
38
38
  "fi",
39
- 'echo "rpp: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
39
+ 'echo "paperlint: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
40
40
  "exit 1",
41
41
  "",
42
42
  ].join("\n");
@@ -8,7 +8,7 @@ import { parseBanalSettings } from "./settings.ts";
8
8
 
9
9
  const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
10
10
  const project = "/p" as AbsolutePath;
11
- const cache = `/h/.cache/rpp/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
11
+ const cache = `/h/.cache/paperlint/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
12
12
  const on =
13
13
  (...files: string[]) =>
14
14
  (p: string) =>
@@ -22,11 +22,11 @@ test("installedBanal: one directory per HotCRP commit", () => {
22
22
  assert.equal(installedBanal(parseBanalSettings({}, dirs)), cache);
23
23
  });
24
24
 
25
- test("rpp's own copy when nothing else is named", () => {
25
+ test("paperlint's own copy when nothing else is named", () => {
26
26
  assert.equal(kind(pick({}, on(cache))), "cache");
27
27
  });
28
28
 
29
- test("a project's vendor/banal before rpp's copy", () => {
29
+ test("a project's vendor/banal before paperlint's copy", () => {
30
30
  // Guards: a project that vendors banal keeps using its own copy.
31
31
  assert.equal(kind(pick({}, on(cache, "/p/vendor/banal"))), "vendor");
32
32
  });
@@ -44,7 +44,7 @@ test("🔴 $BANAL naming a missing file finds NOTHING — no fallback to another
44
44
  );
45
45
  });
46
46
 
47
- test("nothing anywhere: not installed, naming where rpp's copy would be", () => {
47
+ test("nothing anywhere: not installed, naming where paperlint's copy would be", () => {
48
48
  assert.deepEqual(
49
49
  pick({}, on()),
50
50
  err({ kind: "not-installed", installed: cache }),
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ── THE LICENCE BOUNDARY ────────────────────────────────────────────────────────
10
10
  * banal is GPL-2.0-or-later; paperlint is MIT. paperlint therefore never contains banal: `paperlint toolchain`
11
- * DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in rpp's cache, and
11
+ * DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in paperlint's cache, and
12
12
  * paperlint EXECUTES it as a separate program (`perl banal …`), reading its JSON output. Nothing of banal
13
13
  * is copied, vendored, linked or translated into this package.
14
14
  *
@@ -96,7 +96,7 @@ function runBanal(
96
96
  banal: LocatedBanal,
97
97
  pages: readonly PageLayout[],
98
98
  ): Result<BanalMeasurement, BanalFailure> {
99
- return d.workspace.within("rpp-banal-", (scratch) =>
99
+ return d.workspace.within("paperlint-banal-", (scratch) =>
100
100
  parseBanalOutput(
101
101
  d.run.run(
102
102
  banalCommand(
@@ -4,22 +4,28 @@ import { parseBanalSettings } from "./settings.ts";
4
4
 
5
5
  const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
6
6
 
7
- test("cacheDir: $RPP_BANAL_DIR, else $XDG_CACHE_HOME/rpp/banal, else ~/.cache/rpp/banal", () => {
7
+ test("cacheDir: $PAPERLINT_BANAL_DIR, else $XDG_CACHE_HOME/paperlint/banal, else ~/.cache/paperlint/banal", () => {
8
8
  assert.equal(
9
- parseBanalSettings({ RPP_BANAL_DIR: "/x" }, dirs).cacheDir,
9
+ parseBanalSettings({ PAPERLINT_BANAL_DIR: "/x" }, dirs).cacheDir,
10
10
  "/x",
11
11
  );
12
12
  assert.equal(
13
13
  parseBanalSettings({ XDG_CACHE_HOME: "/c" }, dirs).cacheDir,
14
- "/c/rpp/banal",
14
+ "/c/paperlint/banal",
15
+ );
16
+ assert.equal(
17
+ parseBanalSettings({}, dirs).cacheDir,
18
+ "/h/.cache/paperlint/banal",
15
19
  );
16
- assert.equal(parseBanalSettings({}, dirs).cacheDir, "/h/.cache/rpp/banal");
17
20
  });
18
21
 
19
22
  test("an empty variable is unset, and a relative one is relative to the cwd the root read", () => {
20
- const s = parseBanalSettings({ BANAL: "b/banal", RPP_BANAL_DIR: "" }, dirs);
23
+ const s = parseBanalSettings(
24
+ { BANAL: "b/banal", PAPERLINT_BANAL_DIR: "" },
25
+ dirs,
26
+ );
21
27
  assert.equal(s.explicit, "/w/b/banal");
22
- assert.equal(s.cacheDir, "/h/.cache/rpp/banal");
28
+ assert.equal(s.cacheDir, "/h/.cache/paperlint/banal");
23
29
  assert.equal(parseBanalSettings({}, dirs).explicit, null);
24
30
  });
25
31
 
@@ -11,15 +11,15 @@ import {
11
11
  } from "../../domain/host.ts";
12
12
  import type { AbsolutePath } from "../../domain/paths.ts";
13
13
 
14
- /** An explicit banal to use instead of rpp's (a path to the script). */
14
+ /** An explicit banal to use instead of paperlint's (a path to the script). */
15
15
  export const BANAL_ENV = "BANAL";
16
- /** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/rpp/banal`, else `~/.cache/rpp/banal`. */
17
- export const BANAL_DIR_ENV = "RPP_BANAL_DIR";
16
+ /** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/paperlint/banal`, else `~/.cache/paperlint/banal`. */
17
+ export const BANAL_DIR_ENV = "PAPERLINT_BANAL_DIR";
18
18
 
19
19
  export interface BanalSettings {
20
20
  /** `$BANAL`: the user's explicit choice; null when unset. */
21
21
  readonly explicit: AbsolutePath | null;
22
- /** `$RPP_BANAL_DIR` › `$XDG_CACHE_HOME/rpp/banal` › `<home>/.cache/rpp/banal`. */
22
+ /** `$PAPERLINT_BANAL_DIR` › `$XDG_CACHE_HOME/paperlint/banal` › `<home>/.cache/paperlint/banal`. */
23
23
  readonly cacheDir: AbsolutePath;
24
24
  /** Where scratch directories go. */
25
25
  readonly tmpDir: AbsolutePath;
@@ -37,7 +37,11 @@ const set = (v: string | undefined): string | null => (v ? v : null);
37
37
  function cacheDirOf(env: Environment, d: HostDirs): string {
38
38
  return (
39
39
  set(env[BANAL_DIR_ENV]) ??
40
- join(set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"), "rpp", "banal")
40
+ join(
41
+ set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"),
42
+ "paperlint",
43
+ "banal",
44
+ )
41
45
  );
42
46
  }
43
47
 
@@ -47,7 +47,7 @@ const page = (boxes: TextBox[], widthPt = 612, heightPt = 792): PageLayout => ({
47
47
  const expected = [
48
48
  '<?xml version="1.0" encoding="UTF-8"?>',
49
49
  '<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
50
- '<pdf2xml producer="research-paper-pipeline (pdf.js)" version="24.02.0">',
50
+ '<pdf2xml producer="paperlint (pdf.js)" version="24.02.0">',
51
51
  '<page number="1" position="absolute" top="0" left="0" height="2376" width="1836">',
52
52
  '\t<fontspec id="0" size="30" family="LinLibertineT" color="#000000"/>',
53
53
  '<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">Hello</text>',
@@ -104,7 +104,7 @@ export function pdf2xml(pages: readonly PageLayout[]): string {
104
104
  const lines = [
105
105
  '<?xml version="1.0" encoding="UTF-8"?>',
106
106
  '<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
107
- `<pdf2xml producer="research-paper-pipeline (pdf.js)" version="${XML_DIALECT.version}">`,
107
+ `<pdf2xml producer="paperlint (pdf.js)" version="${XML_DIALECT.version}">`,
108
108
  ...pages.flatMap((p, i) => pageXml(p, i + 1, fonts)),
109
109
  "</pdf2xml>",
110
110
  ];
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `Download` through `curl -fsSL`. Not Node's `fetch`: undici's `fetch` ignores `HTTPS_PROXY` unless
3
- * a proxy agent is installed, and rpp's CI and the containers it runs in reach the network only
3
+ * a proxy agent is installed, and paperlint's CI and the containers it runs in reach the network only
4
4
  * through a proxy. curl reads the proxy variables itself. (A claim from documentation, not measured
5
5
  * here — the port is what makes trying the other adapter cheap.)
6
6
  */
@@ -41,7 +41,9 @@ export function curlDownload(o: {
41
41
  }): Download {
42
42
  return {
43
43
  fetch(url: string, timeoutMs: number) {
44
- const dir = realpathSync(mkdtempSync(join(o.tmpDir, "rpp-download-")));
44
+ const dir = realpathSync(
45
+ mkdtempSync(join(o.tmpDir, "paperlint-download-")),
46
+ );
45
47
  const out = join(dir, "body");
46
48
  try {
47
49
  const seconds = String(Math.ceil(timeoutMs / 1000));
@@ -19,7 +19,9 @@ import type { AbsolutePath } from "../../domain/paths.ts";
19
19
  import { curlDownload } from "./download.io.ts";
20
20
  import { spawnProcess } from "../node/index.ts";
21
21
 
22
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-download-test-")));
22
+ const root = realpathSync(
23
+ mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
24
+ );
23
25
  after(() => rmSync(root, { recursive: true, force: true }));
24
26
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
25
27
 
@@ -7,7 +7,7 @@ import { afterAll as after, test } from "vitest";
7
7
  import type { AbsolutePath } from "../../domain/paths.ts";
8
8
  import { nodeFiles } from "./files.io.ts";
9
9
 
10
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-files-test-")));
10
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
11
11
  after(() => rmSync(root, { recursive: true, force: true }));
12
12
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
13
13
 
@@ -19,8 +19,8 @@ const sh = (script: string, timeoutMs = 10_000): Command => ({
19
19
  test("a program that is not there is `not-found`, and names the program", () => {
20
20
  // Guards: ENOENT is the executable itself — "perl is missing" rather than "banal failed".
21
21
  assert.deepEqual(
22
- run.run({ ...sh(""), file: "rpp-no-such-program-xyz", args: [] }),
23
- { kind: "not-found", file: "rpp-no-such-program-xyz" },
22
+ run.run({ ...sh(""), file: "paperlint-no-such-program-xyz", args: [] }),
23
+ { kind: "not-found", file: "paperlint-no-such-program-xyz" },
24
24
  );
25
25
  });
26
26
 
@@ -19,12 +19,12 @@ import { join } from "node:path";
19
19
  import { afterAll as after, test } from "vitest";
20
20
  import { tmpWorkspace } from "./workspace.io.ts";
21
21
 
22
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-ws-test-")));
22
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ws-test-")));
23
23
  after(() => rmSync(root, { recursive: true, force: true }));
24
24
 
25
25
  test("the directory exists inside the scope and is gone after it", () => {
26
26
  const ws = tmpWorkspace(root);
27
- const dir = ws.within("rpp-banal-", (s) => {
27
+ const dir = ws.within("paperlint-banal-", (s) => {
28
28
  assert.ok(existsSync(s.dir));
29
29
  return s.dir;
30
30
  });
@@ -35,7 +35,7 @@ test("the directory exists inside the scope and is gone after it", () => {
35
35
  test("the directory is gone after a throw too", () => {
36
36
  let seen = "";
37
37
  assert.throws(() =>
38
- tmpWorkspace(root).within("rpp-banal-", (s) => {
38
+ tmpWorkspace(root).within("paperlint-banal-", (s) => {
39
39
  seen = s.dir;
40
40
  throw new Error("boom");
41
41
  }),
@@ -46,7 +46,7 @@ test("the directory is gone after a throw too", () => {
46
46
  test("🔴 write: the file lands in the scope's directory, and `exec` makes it runnable — from a path with a space and a quote", () => {
47
47
  const awkward = join(root, "it's a dir");
48
48
  mkdirSync(awkward);
49
- tmpWorkspace(awkward).within("rpp-ws-", (s) => {
49
+ tmpWorkspace(awkward).within("paperlint-ws-", (s) => {
50
50
  const plain = s.write("data.txt", "hello", "read");
51
51
  assert.equal(readFileSync(plain, "utf8"), "hello");
52
52
  const script = s.write("run", "#!/bin/sh\necho ran\n", "exec");
@@ -89,7 +89,7 @@ for (const [a, yes] of [
89
89
  check(`isYes(${JSON.stringify(a)}) is ${yes}`, E.isYes(a) === yes);
90
90
 
91
91
  // ── 2. prepareEngine ────────────────────────────────────────────────────────────────────
92
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-engine-h-")));
92
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-engine-h-")));
93
93
  const TEX = {
94
94
  packages: { acmart: ["acmart.cls"], libertine: ["libertine.sty"] },
95
95
  tools: {},
@@ -144,7 +144,7 @@ const go = async (o) => {
144
144
  };
145
145
  };
146
146
  const envWith = (cacheRoot, path) => ({
147
- RPP_TEXLIVE_DIR: cacheRoot,
147
+ PAPERLINT_TEXLIVE_DIR: cacheRoot,
148
148
  PATH: path,
149
149
  });
150
150
 
@@ -29,7 +29,11 @@ import {
29
29
  usableTree,
30
30
  type InstallResult,
31
31
  } from "./toolchain.ts";
32
- import { declaredUnion, type TexRequirements } from "./tex-requirements.ts";
32
+ import {
33
+ declaredUnion,
34
+ mergeRequirements,
35
+ type TexRequirements,
36
+ } from "./tex-requirements.ts";
33
37
 
34
38
  export interface EngineOptions {
35
39
  /** What the targeted papers need: the base set plus each paper's venue. */
@@ -81,12 +85,16 @@ function withDefaults(o: EngineOptions): Resolved {
81
85
  };
82
86
  return {
83
87
  ...r,
84
- install: o.install ?? (() => ensureTexLive(declaredUnion().tex, r)),
88
+ // The shipped union AND what the targeted papers need: a project's own preset lives outside
89
+ // the package, and its packages are only in `r.tex`.
90
+ install:
91
+ o.install ??
92
+ (() => ensureTexLive(mergeRequirements(declaredUnion().tex, r.tex), r)),
85
93
  };
86
94
  }
87
95
 
88
96
  /**
89
- * The facts `resolveEngine` decides on: rpp's cache and the PATH's TeX Live, each probed. The cache
97
+ * The facts `resolveEngine` decides on: paperlint's cache and the PATH's TeX Live, each probed. The cache
90
98
  * may hold one tree per TeX Live year; the one reported is the newest COMPLETE one, else the newest
91
99
  * (so a new year whose install was interrupted never wins over a complete older year).
92
100
  */
@@ -50,10 +50,10 @@ const check = (label, cond) => {
50
50
  n++;
51
51
  };
52
52
 
53
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-build-")));
53
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-build-")));
54
54
  // 🔴 banal is looked for under the temp root, never in the developer's cache: with the real
55
- // `~/.cache/rpp/banal` present, the measure step would run the real banal on the fake reader's pages.
56
- const ENV = { RPP_BANAL_DIR: join(root, "no-banal") };
55
+ // `~/.cache/paperlint/banal` present, the measure step would run the real banal on the fake reader's pages.
56
+ const ENV = { PAPERLINT_BANAL_DIR: join(root, "no-banal") };
57
57
  const paper = (name, files) => {
58
58
  const dir = join(root, "papers", name);
59
59
  mkdirSync(dir, { recursive: true });
@@ -137,6 +137,24 @@ const fakeRead = async (pdf) => {
137
137
  const CLEAN_TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
138
138
 
139
139
  try {
140
+ // ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
141
+ // First: the facts below resolve a paper's venue preset in this same directory, so a wrong one
142
+ // must be named HERE, not as a missing venue label two checks later.
143
+ const venues = packageVenuesDir();
144
+ check(
145
+ "the venues directory is paperlint's own and holds paper-guards.tex",
146
+ existsSync(join(venues, "paper-guards.tex")),
147
+ );
148
+ check(
149
+ "🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
150
+ withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
151
+ );
152
+ check(
153
+ "an existing TEXINPUTS is kept, after ours",
154
+ withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
155
+ `${venues}${delimiter}/mine${delimiter}`,
156
+ );
157
+
140
158
  // ── facts: parsed from the paper, not configured ──────────────────────────────────────
141
159
  check(
142
160
  "documentclass and options come from the parser",
@@ -157,33 +175,20 @@ try {
157
175
  );
158
176
  const venued = paper("venued", {
159
177
  "paper.tex": CLEAN_TEX,
160
- "venue.json": JSON.stringify({ venue: "agenticdev", kind: "short" }),
178
+ "paperlint.json": JSON.stringify({
179
+ extends: "paperlint:agenticdev",
180
+ kind: "short",
181
+ }),
161
182
  "build.sh": "exit 0\n",
162
183
  });
163
184
  const facts = readFacts(venued);
164
- check("the venue comes from venue.json", facts.venue === "agenticdev");
185
+ check("the venue comes from paperlint.json", facts.venue === "agenticdev");
165
186
  check(
166
187
  "a leftover build.sh is a FACT (reported), not a step",
167
188
  JSON.stringify(facts.ignoredScripts) === JSON.stringify(["build.sh"]) &&
168
189
  IGNORED_SCRIPTS.includes("repro/build-submission.sh"),
169
190
  );
170
191
 
171
- // ── inputs: rpp's own venues directory, no configuration ───────────────────────────────
172
- const venues = packageVenuesDir();
173
- check(
174
- "the venues directory is rpp's own and holds paper-guards.tex",
175
- existsSync(join(venues, "paper-guards.tex")),
176
- );
177
- check(
178
- "🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
179
- withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
180
- );
181
- check(
182
- "an existing TEXINPUTS is kept, after ours",
183
- withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
184
- `${venues}${delimiter}/mine${delimiter}`,
185
- );
186
-
187
192
  // ── the plan ──────────────────────────────────────────────────────────────────────────
188
193
  const plan = planFor(facts);
189
194
  check(
@@ -261,7 +266,7 @@ try {
261
266
  tex.calls.every((c) => c.cwd === clean),
262
267
  );
263
268
  check(
264
- "🔴 pdflatex got rpp's venues directory on TEXINPUTS, trailing separator kept",
269
+ "🔴 pdflatex got paperlint's venues directory on TEXINPUTS, trailing separator kept",
265
270
  tex.calls.every((c) => c.texinputs === `${venues}${delimiter}`),
266
271
  );
267
272
 
@@ -132,9 +132,10 @@ process.exit(
132
132
  {
133
133
  name: "the port resolves the venues directory one level off",
134
134
  harness: HARNESS,
135
- expect: "the venues directory is rpp's own and holds paper-guards.tex",
135
+ expect:
136
+ "the venues directory is paperlint's own and holds paper-guards.tex",
136
137
  disables:
137
- "the one answer to 'where are rpp's venue files'. A wrong directory does not fail " +
138
+ "the one answer to 'where are paperlint's venue files'. A wrong directory does not fail " +
138
139
  "pdflatex by itself — \\input{paper-guards} would fail later, in a paper, far from here",
139
140
  edits: [
140
141
  [