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
@@ -18,8 +18,8 @@ The README carries the minimal version of this. Everything below is the full sur
18
18
  "causeMarker": "Cause:",
19
19
  "rules": [
20
20
  {
21
- "files": ["papers/agenticdev-2026/**"],
22
- "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
21
+ "files": ["papers/old-draft/**"],
22
+ "rules": { "paper/typography": "off" }
23
23
  }
24
24
  ]
25
25
  }
@@ -57,6 +57,61 @@ Until 2026-09-24 this field was called `papers`. The old name is not read as a f
57
57
  hooks stay silent instead. The name is defined once, as `PAPERS_DIR_FIELD` in
58
58
  `lib/paper-config.mjs`.
59
59
 
60
+ ## Three levels of settings
61
+
62
+ Each level is named after the tool, and each says something the others cannot:
63
+
64
+ ```
65
+ package.json "paperlint": { … } the PROJECT: where the papers are, what every paper gets
66
+ papers/
67
+ my-paper/
68
+ paper.tex
69
+ PIPELINE-STATUS.md
70
+ paperlint.json { "extends": … } THIS PAPER: its venue preset, its kind, its own rules — `paperlint new` writes it
71
+ venues/usenix-sec.jsonc (optional, your own) a VENUE PRESET: format, page limits, TeX packages, rules
72
+ node_modules/paperlint/skills/submit-paper/references/venues/
73
+ acm-sigconf.jsonc agenticdev.jsonc aisec.jsonc realm.jsonc the shipped presets (paperlint:<name>)
74
+ ```
75
+
76
+ `paperlint new` writes `<paper>/paperlint.json` from the template (`templates/paper/paperlint.json`,
77
+ or your `<papers>/.template/paperlint.json` if you keep one), with `"extends": null` — no venue chosen
78
+ yet — and a `$comment` saying what goes there. Until `extends` names a preset, `paperlint lint` gives
79
+ that paper one warning, `pdf/measured`: "this paper names no venue preset yet … set "extends" in
80
+ papers/my-paper/paperlint.json". A paper folder with no `paperlint.json` at all — one created before
81
+ 2.1.0 — gets no venue checks and no warning; `npx paperlint new <its name>` adds the file and
82
+ changes nothing else.
83
+
84
+ ```json
85
+ {
86
+ "extends": "paperlint:aisec",
87
+ "kind": "research",
88
+ "rules": { "pdf/body-size": "off" }
89
+ }
90
+ ```
91
+
92
+ | key | what it is |
93
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
94
+ | `extends` | the venue preset the built PDF is judged against: `paperlint:<name>` (shipped) or `./path` / `../path` (your own, relative to this file) — [`rules.md`](rules.md#checks-against-the-venue) |
95
+ | `kind` | the kind of paper (`short`, `research`, …) whose page limit applies |
96
+ | `pdf` | where the built PDF is, relative to the paper, when it is not `paper.pdf` |
97
+ | `rules` | rule id → severity, for this paper alone — the same entries as a `rules` block below |
98
+ | `$comment` | a note for humans (JSON Schema's comment keyword); ignored |
99
+
100
+ Any other key is an error naming the file and the key, as in `package.json`.
101
+
102
+ **Where a paper's rules come from, in order — a later one wins, rule by rule:** paperlint's own
103
+ configuration → the preset chain's `rules`, from the root preset to the one the paper extends →
104
+ the paper's own `rules` → the project's `rules` blocks in `package.json`. So a venue can turn a
105
+ rule on for its papers, a paper can turn it off for itself, and the project can still override
106
+ both. Only rules paperlint ships may be named, at every level.
107
+
108
+ **`paperlint.json` replaces `venue.json` (2.1.0); `npx paperlint init` moves it.** The old name is not
109
+ read: a paper with only a `venue.json` gets a `pdf/profile` error and `paperlint doctor` names the
110
+ file, both pointing at `init`. `init` writes the same settings as `paperlint.json` —
111
+ `"venue": "aisec"` becomes `"extends": "paperlint:aisec"`, and the `"_"` some files used as a
112
+ comment becomes `"$comment"` — then deletes `venue.json`. It removes a `venue.json` whose
113
+ `paperlint.json` already says the same, and refuses, changing nothing, when both exist and differ.
114
+
60
115
  ## Why the key lives in `package.json`
61
116
 
62
117
  Because of a count: the `package.json` key has **five** readers — the three editor hooks, the
@@ -65,12 +120,8 @@ import code and cannot walk up a tree looking for a config; it can read a path i
65
120
  and the one path it can always name is the project's `package.json`.
66
121
 
67
122
  `paperlint lint` looks for it in the current directory and then upwards, the way eslint and tsc find
68
- theirs, and prints which file it found. `--config <file>` overrides the search.
69
-
70
- ⚠️ **`rpp.json` is deprecated and still read.** Earlier versions of `init` created it; `init` no
71
- longer does, and a run that reads one says so on its first line. The hooks never read it, so
72
- leaving settings there is how the linter and the guard end up watching different directories —
73
- `paperlint init` copies the value across for you.
123
+ theirs, and prints which file it found. `--config <file>` overrides the search; the file has the
124
+ same shape, with the settings under the `paperlint` key.
74
125
 
75
126
  ## The `rules` key: turning rules on and off
76
127
 
@@ -80,11 +131,15 @@ the three keys that make sense in JSON — `files`, `ignores` and `rules`. paper
80
131
  **after** its own configuration, so, as in ESLint, a later block wins: a block can turn on a rule
81
132
  that is off by default, or change the severity of one that is on.
82
133
 
134
+ For ONE paper, the paper's own `paperlint.json` is simpler — no glob to get wrong
135
+ ([above](#three-levels-of-settings)). Use a block here for a rule across several papers, or to
136
+ override what a paper says:
137
+
83
138
  ```json
84
139
  "rules": [
85
140
  {
86
- "files": ["papers/agenticdev-2026/**"],
87
- "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
141
+ "files": ["papers/**"],
142
+ "rules": { "pdf/body-size": "off" }
88
143
  },
89
144
  {
90
145
  "files": ["papers/old-draft/**"],
@@ -120,7 +175,7 @@ and the real findings leave with it.
120
175
 
121
176
  ```json
122
177
  "structure": {
123
- "markers": ["PIPELINE-STATUS.md", "paper.tex", "paper.md", "venue.json"],
178
+ "markers": ["PIPELINE-STATUS.md", "paper.tex", "paper.md", "paperlint.json"],
124
179
  "require": ["PIPELINE-STATUS.md"],
125
180
  "requireOneOf": [["paper.tex", "paper.md"]],
126
181
  "ignore": []
@@ -129,7 +184,7 @@ and the real findings leave with it.
129
184
 
130
185
  Those are the defaults; you only write the block to change them. `paper.md` is still in them
131
186
  because Markdown papers are deprecated but not yet removed
132
- ([#57](https://github.com/zernie/research-paper-pipeline/issues/57)). They were measured against a
187
+ ([#57](https://github.com/zernie/paperlint/issues/57)). They were measured against a
133
188
  real five-paper corpus rather than chosen — it passes with zero findings, while adding
134
189
  `paper.pdf` to `require` produces two findings on papers that are perfectly fine, which is why it
135
190
  is not there.
@@ -177,11 +232,12 @@ papers/my-paper
177
232
 
178
233
  The build does **not** judge the layout. A balanced last page, a page limit, the fonts a venue
179
234
  wants — those are verdicts about the finished PDF, and they belong to lint rules that can be
180
- turned on per venue, given a severity and suppressed with a reason. paperlint once searched for a
235
+ turned on per venue, given a severity and suppressed with a reason: the `pdf/` venue rules
236
+ ([`rules.md`](rules.md#checks-against-the-venue)) and the optional `pdf/last-page-balance`. paperlint once searched for a
181
237
  `\balance` position itself and failed the build when none worked; that was removed on
182
238
  2026-09-24.
183
239
 
184
- The class and its options and the venue in `venue.json` are read from the paper and shown in the
240
+ The class and its options and the venue in `paperlint.json` are read from the paper and shown in the
185
241
  plan; later steps decide from them whether they apply.
186
242
 
187
243
  **`paper.pdf` is deleted before anything runs**, for every targeted paper — before the TeX Live is
@@ -216,18 +272,18 @@ having run on it, because a missing build read as nothing to do.
216
272
  ⚠️ **A `build.sh` or `repro/build-submission.sh` in the paper directory is IGNORED.** Earlier
217
273
  versions ran it; `paperlint build` now says one line — `build.sh is ignored — paperlint builds the paper
218
274
  itself` — and builds the paper itself. The `buildScripts` key is ignored the same way.
219
- Why: [#59](https://github.com/zernie/research-paper-pipeline/issues/59).
275
+ Why: [#59](https://github.com/zernie/paperlint/issues/59).
220
276
 
221
277
  ## Using the rules from an existing ESLint config
222
278
 
223
279
  Under the hood `paperlint lint` builds an ESLint flat config and runs it. If your repository already
224
280
  lints with ESLint, you can import the rule modules from `paperlint/eslint-rules/`
225
- and wire them yourself; `bin/rpp.mjs` exports `buildConfig(options, texLanguage)` that returns
281
+ and wire them yourself; `bin/paperlint.mjs` exports `buildConfig(options, texLanguage)` that returns
226
282
  the exact config the CLI uses, so the shortest path is:
227
283
 
228
284
  ```js
229
285
  // eslint.config.mjs
230
- import { buildConfig } from "paperlint/bin/rpp.mjs";
286
+ import { buildConfig } from "paperlint/bin/paperlint.mjs";
231
287
  import { texLanguage } from "paperlint/eslint-rules/latex-language.mjs";
232
288
  export default buildConfig({ minFindings: 3 }, texLanguage);
233
289
  ```
package/docs/e2e.md CHANGED
@@ -35,8 +35,7 @@ temporary tree, **under npm and under pnpm separately**, and drives the installe
35
35
 
36
36
  - the install itself finishes
37
37
  - `paperlint --help` answers with zero
38
- - `paperlint init` declares the papers directory in `package.json`, and does _not_ leave a second
39
- carrier `rpp.json` behind
38
+ - `paperlint init` declares the papers directory in `package.json`
40
39
  - `paperlint init` finishes with zero — its doctor found no discrepancy
41
40
  - `paperlint init` wires the hooks into `.claude/settings.json` — the same commands `hooks.json`
42
41
  publishes, once each — and says they need `npm install` in a fresh clone; a second `init`
@@ -88,10 +87,10 @@ paperlint's own cache, because the runner has no other.
88
87
 
89
88
  ## `test/e2e/toolchain.mjs` — real TeX Live, and only it
90
89
 
91
- `paperlint toolchain` into `$RPP_TEXLIVE_DIR` against real CTAN; a second run must say "nothing to do"
90
+ `paperlint toolchain` into `$PAPERLINT_TEXLIVE_DIR` against real CTAN; a second run must say "nothing to do"
92
91
  within seconds; `--check` must exit 0; then the `acmart` fixture is built with PATH holding `node`
93
92
  only, so no other TeX Live and no PDF tool can stand in, and the PDF must carry Libertine and Biolinum
94
- and no Computer Modern face. Without `RPP_TEXLIVE_DIR` it is a declared skip: installing ~270 MB
93
+ and no Computer Modern face. Without `PAPERLINT_TEXLIVE_DIR` it is a declared skip: installing ~270 MB
95
94
  into a home directory as a side effect of `npm run check` is the unasked install rule 11 forbids.
96
95
  `src/toolchain.harness.mjs` covers the installer's logic (mirror fallback, archive check, time
97
96
  limit, verification, idempotence) against a fake mirror on disk, without the network.
package/docs/install.md CHANGED
@@ -20,6 +20,15 @@ your project's `node_modules`, so it needs the install first.
20
20
  and skill links that pointed into the old package. Until then the old key is still read, with a
21
21
  warning.
22
22
 
23
+ **Upgrading from 2.0.0**: 2.0.0 wrote hook commands that run `node_modules/paperlint/bin/rpp.mjs`,
24
+ a file later versions do not ship, so those hooks stop running. `npx paperlint doctor` names them;
25
+ `npx paperlint init` replaces them and keeps your own commands in the same matcher. The TeX Live
26
+ and banal caches moved from `~/.cache/rpp/` to `~/.cache/paperlint/`, so `npx paperlint toolchain`
27
+ downloads TeX Live once more (delete the old directory afterwards), and the environment variables
28
+ are now `PAPERLINT_TEXLIVE_DIR`, `PAPERLINT_BANAL_DIR` and `PAPERLINT_CTAN_MIRROR` — the `RPP_*`
29
+ names are no longer read. A leftover `rpp.json` is ignored; its settings belong under the
30
+ `"paperlint"` key of `package.json`.
31
+
23
32
  The npm package carries everything: the `paperlint` command, the ESLint rules, the Claude Code skills
24
33
  and hooks, and the scripts the skills run. External programs are separate:
25
34
 
@@ -68,8 +77,7 @@ gets the hooks; the hook commands run files inside `node_modules`, so a fresh cl
68
77
  config; it can only read a path it can spell, and the one it can always spell is
69
78
  `$CLAUDE_PROJECT_DIR/package.json`. That key is read by the three hooks, `eslint-rules/papers.mjs`,
70
79
  `lib/skill-trigger-cases.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`; a separate
71
- `rpp.json` was read only by the CLI. `rpp.json` is still read as a deprecated fallback, and
72
- `paperlint lint` says so.
80
+ config file would be read only by the CLI, so there is none.
73
81
 
74
82
  **Nothing runs at install time.** No postinstall script and no automatic TeX download. npm's rule
75
83
  is that _"the only valid use of install or preinstall scripts is for compilation"_; husky removed
@@ -135,7 +143,7 @@ on the tree the manager laid out. It also checks that every shipped skill is rea
135
143
  directory under a skill's name survives.
136
144
 
137
145
  Yarn Plug'n'Play has no `node_modules`, and the hook commands in `plugin/hooks/hooks.json` name
138
- `${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs`. Supporting it would need
146
+ `${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/paperlint.mjs`. Supporting it would need
139
147
  a different answer to "where is the runtime", not a flag.
140
148
 
141
149
  ## Install size
@@ -1,8 +1,11 @@
1
1
  # Optional rules
2
2
 
3
- Some checks matter only for some venues. paperlint ships them **off**, and you turn them on for the
4
- papers that need them, in the `rules` setting of your `package.json`
5
- ([`configuration.md`](configuration.md#the-rules-key-turning-rules-on-and-off)).
3
+ Some checks matter only for some venues. paperlint ships them **off**. A venue preset that needs one
4
+ turns it on for its papers (AgenticDev's turns on `pdf/last-page-balance`), and a paper can turn one
5
+ on or off for itself in its own `paperlint.json`
6
+ ([`configuration.md`](configuration.md#three-levels-of-settings)). No glob is involved: a paper's
7
+ rules apply to that paper. The `rules` setting of your `package.json` still overrides both, for one
8
+ paper or many ([`configuration.md`](configuration.md#the-rules-key-turning-rules-on-and-off)).
6
9
 
7
10
  | rule | what it checks | who needs it |
8
11
  | ----------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
@@ -12,21 +15,18 @@ papers that need them, in the `rules` setting of your `package.json`
12
15
 
13
16
  ### Turning it on
14
17
 
18
+ A paper that extends `paperlint:agenticdev` has it already, at 120 pt. For any other paper, in its
19
+ `papers/my-paper/paperlint.json`:
20
+
15
21
  ```json
16
22
  {
17
- "paperlint": {
18
- "papersDir": "papers",
19
- "rules": [
20
- {
21
- "files": ["papers/agenticdev-2026/**"],
22
- "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
23
- }
24
- ]
25
- }
23
+ "extends": "paperlint:aisec",
24
+ "kind": "research",
25
+ "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
26
26
  }
27
27
  ```
28
28
 
29
- `files` is relative to the `package.json`. `tolerancePt` is how far apart, in points, the two
29
+ It applies to that paper alone; `"off"` there turns it off for a paper whose preset turns it on. `tolerancePt` is how far apart, in points, the two
30
30
  columns may end; it defaults to 120. On a real accepted paper the balanced build ended 2.7 pt apart
31
31
  and the one the publisher sent back 321.4 pt apart — nothing in between — so the default leaves a
32
32
  wide margin on both sides.
@@ -34,21 +34,22 @@ wide margin on both sides.
34
34
  ### What it reads
35
35
 
36
36
  The rule does not open the PDF to measure it. `paperlint build` measures every PDF it builds and writes
37
- the result to `<paper>/_build/paper.facts.json` ([`configuration.md`](configuration.md#how-rpp-build-compiles-a-paper)),
37
+ the result to `<paper>/_build/paper.facts.json` ([`configuration.md`](configuration.md#how-paperlint-build-compiles-a-paper)),
38
38
  and the rule judges that file, reporting on the paper's `paper.tex` at the `\documentclass` line.
39
39
  So: **build, then lint.**
40
40
 
41
- | the rule finds | it says |
42
- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
43
- | columns further apart than `tolerancePt` | **the finding**, with both heights and how to fix it (below) |
44
- | no `_build/paper.facts.json` | build the paper first |
45
- | facts about a different PDF than the one on disk | the facts are stale (their SHA-256 differs) — rebuild |
46
- | facts whose PDF is gone (a failed build removes it) | rebuild |
47
- | a last page of a few lines | nothing — there is no layout to balance |
48
- | a review build with numbered lines | nothing — the numbers run down the whole page, so both columns measure full height, and balance is a camera-ready requirement |
49
-
50
- If you turn the rule on with a `files` glob that reaches no `paper.tex`, `paperlint lint` fails and says
51
- so, rather than reporting a clean run for a rule that never ran.
41
+ | the rule finds | it says |
42
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
43
+ | columns further apart than `tolerancePt` | **the finding**, with both heights and how to fix it (below) |
44
+ | no `_build/paper.facts.json` | build the paper first — unless the paper extends a venue preset: then `pdf/measured` already warns, once, and this rule is silent |
45
+ | facts about a different PDF than the one on disk | the facts are stale (their SHA-256 differs) — rebuild |
46
+ | facts whose PDF is gone (a failed build removes it) | rebuild |
47
+ | a last page of a few lines | nothing — there is no layout to balance |
48
+ | a review build with numbered lines | nothing — the numbers run down the whole page, so both columns measure full height, and balance is a camera-ready requirement |
49
+
50
+ If you turn the rule on — in a `paperlint.json` beside no `paper.tex`, or with a `files` glob in
51
+ `package.json` that reaches none — `paperlint lint` fails and says so, rather than reporting a clean
52
+ run for a rule that never ran.
52
53
 
53
54
  ### Which venues need it
54
55
 
package/docs/rules.md CHANGED
@@ -14,6 +14,13 @@ reads and when it fails. Errors fail `paperlint lint`; warnings print and do not
14
14
  | `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
15
15
  | `review/findings-cause` | error | `reviews/*.md` | a review lists at least `minFindings` (default 3) findings and no cell introduces a cause with the marker (default `Cause:`) |
16
16
  | `doc/fields` | warn | `reviews/*.md` | a front-matter field is missing or holds a value outside the list you configured. Off entirely unless you configure `docFields` |
17
+ | `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 2, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
18
+ | `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, only a pre-2.1.0 `venue.json` is there (`npx paperlint init` moves it), its `extends` does not resolve (not found, a cycle, a chain longer than four, a preset that fails the schema, an npm name), it names no `kind` while the preset has kinds, or names a kind the preset does not have |
19
+ | `pdf/fonts` | error | `paper.tex` → `_build/paper.facts.json` | a font the pages draw is Type 3 or not embedded, or no font starts with the family the venue preset names for body text (`fonts_text`) or headings (`fonts_title`) |
20
+ | `pdf/geometry` | error | `paper.tex` → `_build/paper.facts.json` | the page width or height is more than `dimTol` (default 0.05 in) off the preset's, or the column count differs |
21
+ | `pdf/limits` | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limit of the paper's kind, or the reference font size is outside the preset's range widened by `body_pt_tol` |
22
+ | `pdf/body-size` | warn | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's. A warning: banal measures the mode of the rendered text, not the declared size (9.30 pt measured at a declared 9) |
23
+ | `pdf/measured` | warn | `paper.tex` → `_build/paper.facts.json` | the paper's `paperlint.json` names no venue preset yet; or it names one and there are no facts (it was not built), or the facts carry no page geometry (banal was not found) — so the checks above did not run |
17
24
 
18
25
  Optional rules — off unless you turn them on in the `rules` setting, because only some venues need
19
26
  them — are on their own page: [`optional-rules.md`](optional-rules.md). Today there is one,
@@ -23,15 +30,137 @@ Besides these rules, `paperlint lint` reports a paper directory that is missing
23
30
  default `PIPELINE-STATUS.md`) as an error. Which files are required is configurable — see
24
31
  [`configuration.md`](configuration.md#required-files).
25
32
 
33
+ ## Checks against the venue
34
+
35
+ A paper says where it is submitted in a `paperlint.json` beside `paper.tex`
36
+ ([`configuration.md`](configuration.md#three-levels-of-settings)):
37
+
38
+ ```json
39
+ { "extends": "paperlint:aisec", "kind": "research" }
40
+ ```
41
+
42
+ `extends` names a **venue preset** — the way an ESLint config extends a shareable config. `kind`
43
+ names the kind of paper, whose page limit applies. A preset holds the numbers from the venue's call
44
+ for papers, each with the quote it came from, the TeX packages its template needs, and the rules
45
+ the venue implies. A preset may itself extend another: the ACM venues extend the `acm-sigconf`
46
+ family, which holds everything the ACM template decides.
47
+
48
+ | preset | extends | template | kinds | page limit checked | rules it turns on |
49
+ | ----------------------- | ----------------------- | -------------------- | -------------------------------------------------------- | ------------------ | ----------------------- |
50
+ | `paperlint:acm-sigconf` | — | ACM `acmart` sigconf | none | no (no kinds) | — |
51
+ | `paperlint:agenticdev` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `short` (5 + 2 refs), `full` (10 + 2), `demo` (5 + 2) | yes | `pdf/last-page-balance` |
52
+ | `paperlint:aisec` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `research`, `benchmark`, `position`, `sok` (10 + 2 each) | yes | — |
53
+ | `paperlint:realm` | — | ACL | `long`, `short` | no — see below | — |
54
+
55
+ That is all that ships today. There is no IEEE, NeurIPS, USENIX or Springer preset. A paper for an
56
+ ACM venue nobody has profiled can extend `paperlint:acm-sigconf` directly: page size, columns and
57
+ fonts are checked, and `pdf/profile` says the page limit is not. For anything else, write your own
58
+ preset ([below](#writing-your-own-venue-preset)). REALM's preset sets no page limit on purpose:
59
+ banal counts the Limitations and Ethics sections as body, ACL does not, and a limit on banal's
60
+ number would fail a correct paper. AgenticDev's turns on `pdf/last-page-balance` because its
61
+ proceedings are produced by Conference Publishing Consulting, which sends back an unbalanced last
62
+ page; AISec's does not, because nothing in hand says who produces the AISec proceedings.
63
+
64
+ **Build, then lint.** The rules judge what `paperlint build` measured and wrote to
65
+ `_build/paper.facts.json` — page count, fonts, and, through banal, page size, columns, font sizes
66
+ and the split into body and reference pages. They report on the paper's `paper.tex`, at the
67
+ `\documentclass` line. What they say when there is nothing to judge, one rule per reason:
68
+
69
+ | the paper | what you get |
70
+ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
71
+ | has a `paperlint.json` that extends no preset (`"extends": null` is what `paperlint new` writes) | `pdf/measured` warning naming the file to set — no venue chosen yet |
72
+ | has no `paperlint.json` at all (a paper made before 2.1.0) | nothing |
73
+ | extends a preset that does not resolve (a typo, a missing file) | `pdf/profile` error, listing the shipped presets |
74
+ | has not been built (no facts file) | `pdf/measured` warning — it does not fail the run, because lint often runs where nothing is built (the CI action only lints) |
75
+ | was built without banal | `pdf/measured` warning; fonts are still checked, the rest is not |
76
+ | has facts about another PDF than the one on disk | `pdf/fresh` error, and nothing else is judged |
77
+ | names no `kind`, or a kind the preset lacks | `pdf/profile` error; everything but the page limit is still checked |
78
+
79
+ The venue comes from `paperlint.json`, not from the facts, so changing it needs no rebuild: the
80
+ measurements do not depend on it. Messages name the venue by the preset's `name`, else by the file
81
+ name of what the paper extends (`paperlint:agenticdev` → `agenticdev`).
82
+
83
+ To skip a check for one paper — a finding you accept — set it to `"off"` in the paper's own
84
+ `paperlint.json`:
85
+
86
+ ```json
87
+ {
88
+ "extends": "paperlint:aisec",
89
+ "kind": "research",
90
+ "rules": { "pdf/body-size": "off" }
91
+ }
92
+ ```
93
+
94
+ ### Writing your own venue preset
95
+
96
+ A preset is a JSONC file (JSON with comments — keep the call-for-papers quote beside each number)
97
+ of the same shape as the shipped ones, validated by
98
+ [`venue-profile.schema.json`](../skills/submit-paper/references/venues/venue-profile.schema.json).
99
+ Put it in your repository and extend it by a path relative to the file that names it:
100
+
101
+ ```
102
+ package.json
103
+ venues/
104
+ usenix-sec.jsonc
105
+ papers/
106
+ usenix-2027/
107
+ paper.tex
108
+ paperlint.json { "extends": "../../venues/usenix-sec.jsonc", "kind": "full" }
109
+ ```
110
+
111
+ ```jsonc
112
+ // venues/usenix-sec.jsonc
113
+ {
114
+ "name": "USENIX Security",
115
+ // a standalone preset needs its template's TeX packages; one that extends a family inherits them
116
+ "template": "article",
117
+ // TeX Live packages only, each with a file that proves it is installed. The USENIX style file
118
+ // itself is not in TeX Live: keep it beside paper.tex.
119
+ "tex": { "packages": { "psnfss": ["times.sty"] } },
120
+ "format": {
121
+ "page_size": "letter",
122
+ "page_w_in": 8.5,
123
+ "page_h_in": 11,
124
+ "columns": 2,
125
+ "body_pt": 10,
126
+ "body_pt_tol": 0.5,
127
+ // the page limit of each kind of paper, as the call for papers states it
128
+ "kinds": { "full": { "body_pages_max": 13 } },
129
+ },
130
+ // only if the proceedings' producer asks for a balanced last page
131
+ "rules": { "pdf/last-page-balance": "error" },
132
+ }
133
+ ```
134
+
135
+ The numbers and packages above illustrate the shape; take yours from the venue's own call for papers and template.
136
+
137
+ | key | what it is |
138
+ | ---------- | --------------------------------------------------------------------------------------------------- |
139
+ | `extends` | the preset this one builds on: `paperlint:<name>` or `./path` / `../path`, relative to this file |
140
+ | `name` | how messages name the venue; the file name otherwise |
141
+ | `template` | the `\documentclass` the venue's template uses |
142
+ | `format` | page size, columns, fonts, font sizes, and `kinds` (page limits per kind of paper) |
143
+ | `tex` | TeX Live packages, each with the files that prove it is installed. Required unless `extends` is set |
144
+ | `rules` | rules the venue implies, rule id → severity or `[severity, options]` |
145
+
146
+ How a chain merges, from the root preset to the paper: `tex` is the union — a child never removes a
147
+ package; `format` keys are replaced one by one, and a child's kind replaces that kind whole;
148
+ `rules` are replaced rule by rule, and the paper's own `rules` come last. A chain is at most four
149
+ presets long, and a cycle is refused by name. `paperlint toolchain` installs the packages of every
150
+ shipped preset and of every preset your papers extend.
151
+
152
+ Presets from npm packages are not supported yet.
153
+
26
154
  ## The paper is LaTeX
27
155
 
28
- The paper body is `paper.tex`, and it gets four rules: `paper/research-question`,
29
- `paper/typography`, `tex/future-promise` and `tex/acm-frontmatter-override`. The scorecard and
156
+ The paper body is `paper.tex`, and it gets four rules of its own: `paper/research-question`,
157
+ `paper/typography`, `tex/future-promise` and `tex/acm-frontmatter-override` — plus the `pdf/`
158
+ venue rules above, which run on it but judge the files beside it. The scorecard and
30
159
  the review notes are Markdown files, and the other five rules read those.
31
160
 
32
161
  A Markdown body (`paper.md`, or `draft.md`) is still read today and gets only the two `paper/`
33
162
  rules, which is why they list it above. Markdown papers are deprecated and being removed
34
- ([#57](https://github.com/zernie/research-paper-pipeline/issues/57)); do not start a new one.
163
+ ([#57](https://github.com/zernie/paperlint/issues/57)); do not start a new one.
35
164
 
36
165
  ## The scorecard's `bytes:` and `sourceBytes:`
37
166
 
package/docs/toolchain.md CHANGED
@@ -36,12 +36,12 @@ The command has two halves, TeX Live and banal (below). Both always run, so one
36
36
  hide the other, and the exit code is 0 only when both are ready.
37
37
 
38
38
  It downloads `install-tl` from a CTAN mirror (four in turn; each download has its own time limit,
39
- TLS is always verified), installs `scheme-basic` into `~/.cache/rpp/texlive/<TeX Live year>`
40
- (`$XDG_CACHE_HOME/rpp/texlive` when that is set, `$RPP_TEXLIVE_DIR` over both), and `tlmgr install`s
39
+ TLS is always verified), installs `scheme-basic` into `~/.cache/paperlint/texlive/<TeX Live year>`
40
+ (`$XDG_CACHE_HOME/paperlint/texlive` when that is set, `$PAPERLINT_TEXLIVE_DIR` over both), and `tlmgr install`s
41
41
  every package the venue profiles declare. Then it checks the RESULT: `kpsewhich` must find every
42
42
  file each profile names, and each declared tool must be an executable in the bin directory. A gap
43
43
  fails the command and names the package and its file. Linux and macOS; on Windows it refuses, and
44
- `paperlint build` uses a TeX Live on PATH that has the packages. `RPP_CTAN_MIRROR` names one mirror
44
+ `paperlint build` uses a TeX Live on PATH that has the packages. `PAPERLINT_CTAN_MIRROR` names one mirror
45
45
  (a `…/systems/texlive/tlnet` URL) to use instead of the list.
46
46
 
47
47
  Measured 2026-09-24 from an empty directory: **3 min 04 s, 269 MB** (du: 298 MB), TeX Live 2026, 47
@@ -109,8 +109,8 @@ therefore does not contain banal. `paperlint toolchain` downloads it from HotCRP
109
109
  | version | banal 1.2 |
110
110
  | sha256 | `fd8cc4ae189b9da02460ae442a34f14434e5784210489fb668313ac671006911` |
111
111
 
112
- It refuses a file with any other sha256, stores it in `~/.cache/rpp/banal/<commit>/banal`
113
- (`$XDG_CACHE_HOME/rpp/banal` when set, `$RPP_BANAL_DIR` over both), and accepts it only after banal
112
+ It refuses a file with any other sha256, stores it in `~/.cache/paperlint/banal/<commit>/banal`
113
+ (`$XDG_CACHE_HOME/paperlint/banal` when set, `$PAPERLINT_BANAL_DIR` over both), and accepts it only after banal
114
114
  has run on a one-page probe and measured it. paperlint then runs it as a separate program — `perl banal
115
115
  -no-time -json <file>.xml` — and reads its JSON output. Nothing of banal is copied, linked or
116
116
  translated into this package.
@@ -27,7 +27,7 @@
27
27
  * two (blanking preserves length), so scanning `raw` and reporting the offset found there
28
28
  * still points at the right byte.
29
29
  *
30
- * ── WHY `bareDecimal` READS NEITHER, BUT THE PARSED TREE (rpp#44) ─────────────────────────
30
+ * ── WHY `bareDecimal` READS NEITHER, BUT THE PARSED TREE (paperlint#44) ─────────────────────────
31
31
  * Origin. On a workshop paper (HotCRP #20) Reviewer B wrote: `Numbers should be completed:
32
32
  * e.g. ".05" -> 0.05`. The submitted PDF really carried `p < .05`, `p=.002`, `p=.006` and
33
33
  * `p=.037` in its text (measured with pdftotext on the submitted version); the camera-ready
@@ -14,7 +14,7 @@
14
14
  * fixture exists, not that the count is correct.
15
15
  *
16
16
  * The bare-decimal count is the exception, because what it gets wrong is not the lexeme but the
17
- * INPUT: rpp#44 was a correct regex run over the wrong text. So two cases below change what it
17
+ * INPUT: paperlint#44 was a correct regex run over the wrong text. So two cases below change what it
18
18
  * reads — back to the raw source (the SILENT half must die), and with math dropped from the walk
19
19
  * (the CAUGHT half must die, since math is where p-values live).
20
20
  */
@@ -85,6 +85,7 @@ import {
85
85
  CONFIG_KEY,
86
86
  DEFAULT_PAPERS_ROOT,
87
87
  PAPERS_DIR_FIELD,
88
+ PAPER_SETTINGS_FILE,
88
89
  declaredSettings,
89
90
  renamedFieldMessage,
90
91
  settingsOf,
@@ -154,8 +155,11 @@ export function paperFiles(root) {
154
155
  tex: [`${root}/*/paper.tex`],
155
156
  /** The per-paper stage ledger. */
156
157
  status: [`${root}/*/PIPELINE-STATUS.md`],
157
- /** The venue data card. */
158
- venue: [`${root}/*/venue.json`],
158
+ /**
159
+ * The per-paper settings file, `paperlint.json` (`venue.json` before 2.1.0). The key keeps its
160
+ * old name so a consumer's config that lints these files keeps matching them.
161
+ */
162
+ venue: [`${root}/*/${PAPER_SETTINGS_FILE}`],
159
163
  /** Facts extracted from the built PDF. */
160
164
  pdfFacts: [`${root}/*/_build/paper.facts.json`],
161
165
  /** Facts extracted from the bibliography. */
@@ -2,7 +2,7 @@
2
2
  * Colocated test for `eslint-rules/pdf-last-page-balance.mjs` (`pdf/last-page-balance`).
3
3
  * Run: `npx vigiles test eslint-rules/pdf-last-page-balance.harness.mjs`
4
4
  *
5
- * The rule is driven the way a consumer drives it: through rpp's own `buildConfig`, with the
5
+ * The rule is driven the way a consumer drives it: through paperlint's own `buildConfig`, with the
6
6
  * rule turned on by a `rules` block in the settings — not through a config invented here. Each
7
7
  * paper is a temporary directory holding `paper.tex`, `paper.pdf` and the facts `paperlint build`
8
8
  * would write beside them.
@@ -51,7 +51,9 @@ const facts = (lastPage, over = {}) => ({
51
51
  });
52
52
  const MEASURED = (l, r) => ({ kind: "measured", columns_pt: [l, r] });
53
53
 
54
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-balance-rule-")));
54
+ const root = realpathSync(
55
+ mkdtempSync(join(tmpdir(), "paperlint-balance-rule-")),
56
+ );
55
57
  /** A paper directory with paper.tex, paper.pdf and, unless `null`, `_build/paper.facts.json`. */
56
58
  const paper = (name, f, { pdf = PDF } = {}) => {
57
59
  const dir = join(root, "papers", name);
@@ -67,7 +69,7 @@ const paper = (name, f, { pdf = PDF } = {}) => {
67
69
  return dir;
68
70
  };
69
71
 
70
- /** Lint files with rpp's config, the rule turned on for `papers/**` (unless `rules` says otherwise). */
72
+ /** Lint files with paperlint's config, the rule turned on for `papers/**` (unless `rules` says otherwise). */
71
73
  async function lint(
72
74
  files,
73
75
  rules = [
@@ -29,12 +29,14 @@
29
29
  *
30
30
  * Silent, by design, where there is nothing to judge: a stub last page (a few lines) and a review
31
31
  * build (numbered lines in the margins make both columns measure full height). Loud where the input
32
- * is missing: no facts file, a foreign schema, facts about a PDF that is not on disk or changed
32
+ * is missing: no facts file (unless the paper extends a venue preset — then `pdf/measured` says it),
33
+ * a foreign schema, facts about a PDF that is not on disk or changed
33
34
  * since — a rule that is on and reads nothing must not look like a rule that passed.
34
35
  */
35
36
  import { createHash } from "node:crypto";
36
37
  import { existsSync, readFileSync } from "node:fs";
37
38
  import { basename, dirname, isAbsolute, join } from "node:path";
39
+ import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
38
40
 
39
41
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
40
42
  export const DEFAULT_TOLERANCE_PT = 120;
@@ -133,11 +135,30 @@ function reportLine(file) {
133
135
  }
134
136
  }
135
137
 
138
+ /**
139
+ * Whether the paper extends a venue preset. Then the venue rules run on it too, and `pdf/measured`
140
+ * already says — once, as a warning — that a paper with no facts was not checked; this rule
141
+ * repeating it as an error would fail a lint-only CI for every paper of a venue whose preset turns
142
+ * this rule on.
143
+ */
144
+ function extendsPreset(paperDir) {
145
+ try {
146
+ const s = JSON.parse(
147
+ readFileSync(join(paperDir, PAPER_SETTINGS_FILE), "utf8"),
148
+ );
149
+ return typeof s?.extends === "string" && s.extends !== "";
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
136
155
  /** Everything the rule decides for one paper, as a finding or null. */
137
156
  function verdict(paperDir, tolerancePt) {
138
157
  const factsFile = join(paperDir, FACTS_REL);
139
158
  if (!existsSync(factsFile))
140
- return { messageId: "noFacts", data: { file: FACTS_REL } };
159
+ return extendsPreset(paperDir)
160
+ ? null
161
+ : { messageId: "noFacts", data: { file: FACTS_REL } };
141
162
  const facts = parseFacts(readFileSync(factsFile, "utf8"));
142
163
  if (!facts.ok) return { messageId: facts.messageId, data: facts.data };
143
164
  return staleness(paperDir, facts) ?? judgeColumns(facts.last, tolerancePt);
@@ -0,0 +1 @@
1
+ { "extends": "paperlint:agenticdev", "kind": "short" }
@@ -1,4 +1,4 @@
1
- % `\input{paper-guards}` resolves with NO configuration: rpp puts its own venues directory on
1
+ % `\input{paper-guards}` resolves with NO configuration: paperlint puts its own venues directory on
2
2
  % TEXINPUTS. The guards fire only on the final pass (\finalpass), so a clean paper with a
3
3
  % forward \ref must still build.
4
4
  \documentclass{article}