paperlint 2.0.1 → 3.0.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 (226) hide show
  1. package/CLAUDE.md +6 -1
  2. package/CONTRIBUTING.md +21 -0
  3. package/README.md +231 -180
  4. package/action.yml +4 -6
  5. package/dist/adapters/references/index.d.ts +13 -0
  6. package/dist/adapters/references/index.d.ts.map +1 -0
  7. package/dist/adapters/references/index.js +55 -0
  8. package/dist/adapters/references/index.js.map +1 -0
  9. package/dist/adapters/references/reach.io.d.ts +3 -0
  10. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  11. package/dist/adapters/references/reach.io.js +14 -0
  12. package/dist/adapters/references/reach.io.js.map +1 -0
  13. package/dist/build-engine.d.ts.map +1 -1
  14. package/dist/build-engine.js +5 -2
  15. package/dist/build-engine.js.map +1 -1
  16. package/dist/build.d.ts +16 -1
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/build.js +40 -5
  19. package/dist/build.js.map +1 -1
  20. package/dist/cli.d.ts +37 -20
  21. package/dist/cli.d.ts.map +1 -1
  22. package/dist/cli.js +377 -251
  23. package/dist/cli.js.map +1 -1
  24. package/dist/doctor.d.ts.map +1 -1
  25. package/dist/doctor.js +37 -52
  26. package/dist/doctor.js.map +1 -1
  27. package/dist/facts-file.d.ts +33 -9
  28. package/dist/facts-file.d.ts.map +1 -1
  29. package/dist/facts-file.js +117 -17
  30. package/dist/facts-file.js.map +1 -1
  31. package/dist/hooks-settings.d.ts +0 -21
  32. package/dist/hooks-settings.d.ts.map +1 -1
  33. package/dist/hooks-settings.js +17 -135
  34. package/dist/hooks-settings.js.map +1 -1
  35. package/dist/init.d.ts +40 -19
  36. package/dist/init.d.ts.map +1 -1
  37. package/dist/init.js +144 -109
  38. package/dist/init.js.map +1 -1
  39. package/dist/link-skills.d.ts +1 -1
  40. package/dist/link-skills.d.ts.map +1 -1
  41. package/dist/link-skills.js +11 -29
  42. package/dist/link-skills.js.map +1 -1
  43. package/dist/new-paper.d.ts +2 -2
  44. package/dist/new-paper.d.ts.map +1 -1
  45. package/dist/new-paper.js +6 -2
  46. package/dist/new-paper.js.map +1 -1
  47. package/dist/paper-files.d.ts +63 -0
  48. package/dist/paper-files.d.ts.map +1 -0
  49. package/dist/paper-files.js +69 -0
  50. package/dist/paper-files.js.map +1 -0
  51. package/dist/paper-settings.d.ts +40 -0
  52. package/dist/paper-settings.d.ts.map +1 -0
  53. package/dist/paper-settings.js +144 -0
  54. package/dist/paper-settings.js.map +1 -0
  55. package/dist/ports/check-references.d.ts +31 -0
  56. package/dist/ports/check-references.d.ts.map +1 -0
  57. package/dist/ports/check-references.js +7 -0
  58. package/dist/ports/check-references.js.map +1 -0
  59. package/dist/presets.d.ts +84 -0
  60. package/dist/presets.d.ts.map +1 -0
  61. package/dist/presets.js +184 -0
  62. package/dist/presets.js.map +1 -0
  63. package/dist/reference-rules.d.ts +47 -0
  64. package/dist/reference-rules.d.ts.map +1 -0
  65. package/dist/reference-rules.js +156 -0
  66. package/dist/reference-rules.js.map +1 -0
  67. package/dist/references.d.ts +36 -0
  68. package/dist/references.d.ts.map +1 -0
  69. package/dist/references.js +91 -0
  70. package/dist/references.js.map +1 -0
  71. package/dist/rules-config.d.ts +11 -2
  72. package/dist/rules-config.d.ts.map +1 -1
  73. package/dist/rules-config.js +25 -11
  74. package/dist/rules-config.js.map +1 -1
  75. package/dist/structure.d.ts.map +1 -1
  76. package/dist/structure.js +3 -2
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.d.ts +63 -7
  79. package/dist/tex-requirements.d.ts.map +1 -1
  80. package/dist/tex-requirements.js +91 -24
  81. package/dist/tex-requirements.js.map +1 -1
  82. package/dist/types.d.ts +20 -18
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/venue-rules.d.ts +109 -0
  85. package/dist/venue-rules.d.ts.map +1 -0
  86. package/dist/venue-rules.js +389 -0
  87. package/dist/venue-rules.js.map +1 -0
  88. package/docs/configuration.md +152 -77
  89. package/docs/e2e.md +1 -1
  90. package/docs/install.md +25 -40
  91. package/docs/optional-rules.md +26 -24
  92. package/docs/rules.md +181 -15
  93. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  94. package/eslint-rules/latex-language.mjs +29 -1
  95. package/eslint-rules/paper-stages.harness.mjs +1 -117
  96. package/eslint-rules/paper-stages.mjs +0 -120
  97. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  98. package/eslint-rules/paper-typography.mjs +267 -192
  99. package/eslint-rules/paper-typography.test.mjs +297 -0
  100. package/eslint-rules/papers.harness.mjs +8 -12
  101. package/eslint-rules/papers.mjs +17 -27
  102. package/eslint-rules/pdf-last-page-balance.mjs +23 -5
  103. package/eslint-rules/review-frontmatter.mjs +118 -0
  104. package/eslint-rules/review-frontmatter.schema.json +36 -0
  105. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  106. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  107. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  108. package/eslint.config.mjs +0 -15
  109. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  110. package/fixtures/real-markdown-paper/baseline.json +6 -7
  111. package/hooks/hooks.harness.mjs +39 -98
  112. package/hooks/hooks.mutations.mjs +6 -6
  113. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  114. package/hooks/paper-edit-guard.hook.mjs +31 -42
  115. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  116. package/hooks/paper-status-gates.hook.mjs +21 -19
  117. package/hooks/paper-status-gates.sh +8 -13
  118. package/lib/paper-config.d.mts +15 -18
  119. package/lib/paper-config.harness.mjs +18 -35
  120. package/lib/paper-config.mjs +90 -81
  121. package/lib/paper-config.mutations.mjs +2 -2
  122. package/lib/skill-checks.mjs +1 -1
  123. package/lib/skill-trigger-cases.harness.mjs +9 -8
  124. package/lib/skill-trigger-cases.mjs +7 -15
  125. package/package.json +1 -1
  126. package/scripts/eslint-report-guard.mjs +1 -1
  127. package/scripts/harness-api.frozen.json +0 -3
  128. package/scripts/mutation-batteries.frozen.json +4 -13
  129. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  130. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  131. package/skills/cold-read-diff/SKILL.md +20 -1
  132. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  133. package/skills/find-venue/SKILL.md +5 -1
  134. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  135. package/skills/grade-paper-writing/SKILL.md +20 -1
  136. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  137. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  138. package/skills/paper-pipeline/SKILL.md +4 -3
  139. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  140. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  141. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  142. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  144. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  145. package/skills/plan-paper-timeline/SKILL.md +2 -2
  146. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  147. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  148. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  149. package/skills/render-paper/check-render.sh +2 -3
  150. package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
  151. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  152. package/skills/render-paper/render-paper.harness.mjs +2 -2
  153. package/skills/study-accepted-papers/SKILL.md +2 -1
  154. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  155. package/skills/submit-paper/SKILL.md +9 -1
  156. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  157. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  158. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  159. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  160. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  161. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  162. package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
  163. package/skills/verify-citations/SKILL.md +10 -1
  164. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  165. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  166. package/src/adapters/references/index.ts +102 -0
  167. package/src/adapters/references/reach.io.ts +12 -0
  168. package/src/build-engine.ts +10 -2
  169. package/src/build.harness.mjs +31 -22
  170. package/src/build.ts +65 -8
  171. package/src/cli.harness.mjs +151 -294
  172. package/src/cli.mutations.mjs +35 -44
  173. package/src/cli.ts +457 -288
  174. package/src/doctor.harness.mjs +41 -84
  175. package/src/doctor.mutations.mjs +9 -9
  176. package/src/doctor.ts +41 -61
  177. package/src/facts-file.test.ts +47 -7
  178. package/src/facts-file.ts +145 -24
  179. package/src/hooks-settings.harness.mjs +7 -92
  180. package/src/hooks-settings.mutations.mjs +2 -17
  181. package/src/hooks-settings.ts +18 -162
  182. package/src/init.test.ts +206 -0
  183. package/src/init.ts +193 -159
  184. package/src/link-skills.harness.mjs +1 -38
  185. package/src/link-skills.mutations.mjs +9 -5
  186. package/src/link-skills.ts +10 -34
  187. package/src/lint-fix.test.ts +120 -0
  188. package/src/new-paper.harness.mjs +9 -6
  189. package/src/new-paper.test.ts +133 -0
  190. package/src/new-paper.ts +7 -2
  191. package/src/paper-files.test.ts +290 -0
  192. package/src/paper-files.ts +144 -0
  193. package/src/paper-settings-commands.test.ts +274 -0
  194. package/src/paper-settings.test.ts +249 -0
  195. package/src/paper-settings.ts +208 -0
  196. package/src/ports/check-references.ts +28 -0
  197. package/src/presets.test.ts +236 -0
  198. package/src/presets.ts +297 -0
  199. package/src/reference-rules.ts +229 -0
  200. package/src/references.test.ts +209 -0
  201. package/src/references.ts +138 -0
  202. package/src/rules-config.harness.mjs +24 -8
  203. package/src/rules-config.ts +30 -13
  204. package/src/structure.harness.mjs +6 -4
  205. package/src/structure.ts +3 -2
  206. package/src/tex-requirements.harness.mjs +11 -17
  207. package/src/tex-requirements.ts +179 -29
  208. package/src/types.ts +30 -18
  209. package/src/venue-rules.test.ts +554 -0
  210. package/src/venue-rules.ts +571 -0
  211. package/templates/paper/paperlint.json +4 -0
  212. package/eslint-rules/doc-fields.harness.mjs +0 -336
  213. package/eslint-rules/doc-fields.mjs +0 -186
  214. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  215. package/eslint-rules/paper-typography.harness.mjs +0 -291
  216. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  217. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  218. package/eslint-rules/review-findings-cause.mjs +0 -135
  219. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  220. package/fixtures/build-e2e/acmart/venue.json +0 -1
  221. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  222. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  223. package/fixtures/review-findings-cause/clean.md +0 -17
  224. package/fixtures/review-findings-cause/defect.md +0 -14
  225. package/fixtures/review-findings-cause/old-debt.md +0 -14
  226. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -1,103 +1,175 @@
1
1
  # Configuration reference
2
2
 
3
- One key in your `package.json`, written by `paperlint init`. It holds the facts only your repository can
4
- supply — nothing in it is guessable by a package that has never seen your corpus.
3
+ paperlint reads one file name at two levels, `paperlint.json`, with one schema. Both files are
4
+ optional: a project with its papers in `papers/` and no special wishes needs neither.
5
5
 
6
- The README carries the minimal version of this. Everything below is the full surface, moved out on
7
- 2026-09-19.
6
+ ```
7
+ paperlint.json the PROJECT (optional): where the papers are, the project's rules,
8
+ defaults for every paper
9
+ papers/
10
+ my-paper/
11
+ paper.tex
12
+ PIPELINE-STATUS.md
13
+ paperlint.json THIS PAPER: its venue preset, its kind, its own rules — `paperlint new` writes it
14
+ venues/usenix-sec.jsonc (optional, your own) a VENUE PRESET: format, page limits, TeX packages, rules
15
+ ```
16
+
17
+ The shipped presets (`paperlint:<name>`) live in the package, under
18
+ `skills/submit-paper/references/venues/`: `acm-sigconf`, `agenticdev`, `aisec`, `realm`.
19
+
20
+ ## The root `paperlint.json`
21
+
22
+ It sits beside your `package.json`. Without it every setting has its default.
8
23
 
9
24
  ```json
10
25
  {
11
- "paperlint": {
12
- "papersDir": "papers",
13
- "authorListCommand": "node scripts/bib-authors.mjs",
14
- "typographyDebt": { "papers/my-paper": { "sectionSign": 12 } },
15
- "docFields": { "read": { "values": ["full", "abstract", "none"] } },
16
- "reviewSince": "2026-08-23",
17
- "minFindings": 3,
18
- "causeMarker": "Cause:",
19
- "rules": [
20
- {
21
- "files": ["papers/agenticdev-2026/**"],
22
- "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
23
- }
24
- ]
25
- }
26
+ "papersDir": "docs/papers",
27
+ "extends": "paperlint:acm-sigconf",
28
+ "rules": [
29
+ {
30
+ "files": ["docs/papers/old-draft/**"],
31
+ "rules": { "paper/section-word": "off" }
32
+ }
33
+ ]
26
34
  }
27
35
  ```
28
36
 
29
- | key | required | what it is |
30
- | ------------------- | -------- | ----------------------------------------------------------------------------------------- |
31
- | `papersDir` | **yes** | the directory your papers live in, relative to the file holding it. One string or a list. |
32
- | `structure` | no | which files every paper directory must contain — see below. `false` turns it off. |
33
- | `authorListCommand` | no | the command `paper/author-list` tells you to run when the check is missing |
34
- | `typographyDebt` | no | per-paper allowance of existing typography findings, so the count can only go down |
35
- | `docFields` | no | required front-matter fields in review files, and the values each may hold |
36
- | `reviewSince` | no | only review files created on or after this date are checked |
37
- | `minFindings` | no | a review with fewer findings than this is not required to name causes |
38
- | `causeMarker` | no | the phrase a review uses to introduce a cause (default `Cause:`), in any language |
39
- | `rules` | no | extra ESLint config blocks: turn optional rules on, change a rule's severity — see below |
40
-
41
- The skill scripts read a few more keys of the same object — `ledger`, `scripts`, `timezone`,
42
- `contactEmail`, `citeChecks`, `triggerCases` — documented with the skills that use them.
43
-
44
- **Any other key is an error**, named in the message: `package.json → "paperlint":
45
- unknown key "typographyDept"`. A misspelt key would otherwise read as "not set", and the setting
46
- you meant would silently do nothing. The list of known keys is `SETTINGS_KEYS` in
47
- `lib/paper-config.mjs`.
48
-
49
- `papersDir` is required because the scope is the one thing that must not default: a default of `"."`
50
- turns every run into a green report over the whole checkout. `paperlint init` fills it by measuring —
51
- and when nothing on disk looks like a papers directory, it writes the documented default and says
52
- in the same breath that it is a guess.
53
-
54
- Until 2026-09-24 this field was called `papers`. The old name is not read as a fallback: `paperlint lint`,
55
- `paperlint init`, `paperlint doctor`, the ESLint helper and the edit guard all stop with
56
- `"papers" was renamed to "papersDir" in package.json → "paperlint"`. The two advisory
57
- hooks stay silent instead. The name is defined once, as `PAPERS_DIR_FIELD` in
58
- `lib/paper-config.mjs`.
59
-
60
- ## Why the key lives in `package.json`
61
-
62
- Because of a count: the `package.json` key has **five** readers — the three editor hooks, the
63
- ESLint helper, the skill scripts — and a separate config file had **one**, the CLI. A hook cannot
64
- import code and cannot walk up a tree looking for a config; it can read a path it is able to name,
65
- and the one path it can always name is the project's `package.json`.
66
-
67
- `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; the file has the
69
- same shape, with the settings under the `paperlint` key.
37
+ | key | default | what it is |
38
+ | ----------- | ---------- | ------------------------------------------------------------------------------------------ |
39
+ | `papersDir` | `"papers"` | the directory your papers live in, relative to this file. One string or a list. Root only. |
40
+ | `structure` | see below | which files every paper directory must contain. `false` turns it off. Root only. |
41
+ | `rules` | none | rule overrides — see [below](#the-rules-key-turning-rules-on-and-off) |
42
+ | `extends` | none | the venue preset for every paper that names none |
43
+ | `kind` | none | the kind of paper for every paper that names none |
44
+ | `pdf` | none | where the built PDF is, relative to each paper, when it is not `paper.pdf` |
45
+ | `$comment` | — | a note for humans (JSON Schema's comment keyword); ignored |
46
+
47
+ The skill scripts read a few more root keys — `ledger`, `scripts`, `timezone`, `contactEmail`,
48
+ `citeChecks`, `triggerCases` — documented with the skills that use them.
49
+
50
+ **`paperlint lint` finds the file** by walking up from the current directory to the nearest
51
+ `paperlint.json` that is not a paper's own (a paper's sits beside its `paper.tex`); if there is
52
+ none, the project root is the nearest directory with a `package.json`. It prints which file it
53
+ found. `--config <file>` names another file of the same shape.
54
+
55
+ **No papers where `papersDir` points is an error**, not a clean run:
56
+
57
+ ```
58
+ no papers in papers/ — create one with `npx paperlint new <name>`, or set "papersDir" in paperlint.json if your papers live elsewhere
59
+ ```
60
+
61
+ `paperlint init` writes the root file only when something differs from the defaults: it measures
62
+ where your papers are, and writes `{ "papersDir": … }` only when that is not `papers`. A
63
+ `papersDir` already declared is kept, and nothing is measured or asked.
64
+
65
+ **Under `papersDir`, only the files paperlint's own rules are written for are linted:**
66
+ `PIPELINE-STATUS.md`, `paper.md`, `draft.md`, `paper.tex`, `reviews/*.md` and `siblings/*.md`.
67
+ Everything else — a paper's `repro/` scripts, vendored JavaScript, data files — is never handed to
68
+ ESLint, so it cannot fail the run. A file you name on the command line that is not one of these is
69
+ refused by name. `node_modules/`, `.git/` and `<papers>/.template/` are skipped.
70
+
71
+ ## A paper's `paperlint.json`
72
+
73
+ ```json
74
+ {
75
+ "extends": "paperlint:aisec",
76
+ "kind": "research",
77
+ "rules": { "pdf/body-size": "off" }
78
+ }
79
+ ```
80
+
81
+ The same keys as the root file, minus the project-only ones (`papersDir`, `structure` and the
82
+ skills' keys), which are refused here by name. **It merges over the root file:** its `extends`,
83
+ `kind` and `pdf` win; one it does not set comes from the root.
84
+
85
+ | key | what it is |
86
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
87
+ | `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) |
88
+ | `kind` | the kind of paper (`short`, `research`, …) whose page limit applies |
89
+ | `pdf` | where the built PDF is, relative to the paper, when it is not `paper.pdf` |
90
+ | `rules` | rule overrides for this paper alone — `{ "<rule>": "<severity>" }`, or blocks with globs relative to the paper |
91
+ | `$comment` | a note for humans; ignored |
92
+
93
+ `paperlint new` writes this file from the template (`templates/paper/paperlint.json`, or your
94
+ `<papers>/.template/paperlint.json` if you keep one), with `"extends": null` — no venue chosen yet —
95
+ and a `$comment` saying what goes there. Until `extends` names a preset (here or in the root file),
96
+ `paperlint lint` gives that paper one warning, `pdf/measured`: "this paper names no venue preset
97
+ yet … set "extends" in papers/my-paper/paperlint.json".
98
+
99
+ **Any other key is an error**, in either file, named in the message: `paperlint.json: unknown key
100
+ "papersdir"`. A misspelt key would otherwise read as "not set", and the setting you meant would
101
+ silently do nothing. The key list is `SETTINGS_KEYS` in `lib/paper-config.mjs`.
102
+
103
+ **Where a paper's rules come from, in order — a later one wins, rule by rule:** paperlint's own
104
+ configuration → the preset chain's `rules`, from the root preset to the one the paper extends →
105
+ the root `paperlint.json`'s `rules` → the paper's own `rules`. So a venue can turn a rule on, the
106
+ project can change that for every paper, and one paper can still change it for itself. Only rules
107
+ paperlint ships may be named, at every level.
108
+
109
+ ## Records in frontmatter, checked by shipped JSON Schemas
110
+
111
+ Two kinds of file under a paper keep a record in their YAML frontmatter, and paperlint validates
112
+ each against a JSON Schema it ships (`eslint-rules/*.schema.json`):
113
+
114
+ - **a review** under `reviews/` (`review/frontmatter`, error) — its findings, each with a status and,
115
+ when open, the pipeline cause that let it through:
116
+
117
+ ```yaml
118
+ ---
119
+ findings:
120
+ - id: 1
121
+ status: open # open | fixed | wontfix
122
+ cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
123
+ ---
124
+ ```
125
+
126
+ A review with no `findings` key is not checked for findings.
127
+
128
+ - **a sibling card** under `siblings/` (`sibling/frontmatter`, warn; `siblings/README.md` is the index,
129
+ not a card) — how much of the competing paper was actually read: `read: full | abstract | none`. A
130
+ card without it, or without frontmatter, is a finding.
70
131
 
71
132
  ## The `rules` key: turning rules on and off
72
133
 
73
- `rules` is a list of blocks in ESLint's own
74
- [flat-config shape](https://eslint.org/docs/latest/use/configure/configuration-files), limited to
75
- the three keys that make sense in JSON — `files`, `ignores` and `rules`. paperlint appends the blocks
76
- **after** its own configuration, so, as in ESLint, a later block wins: a block can turn on a rule
77
- that is off by default, or change the severity of one that is on.
134
+ `rules` takes two shapes, in either file:
135
+
136
+ - **`{ "<rule>": "<severity>" }`** — for every paper file in the file's scope: every paper in the
137
+ root file, that one paper in a paper's file. The simple form, with no glob to get wrong.
138
+ - **a list of blocks** in ESLint's own
139
+ [flat-config shape](https://eslint.org/docs/latest/use/configure/configuration-files), limited to
140
+ the three keys that make sense in JSON — `files`, `ignores` and `rules` — for a rule across some
141
+ papers but not others.
142
+
143
+ paperlint applies them **after** its own configuration, so, as in ESLint, a later one wins: they
144
+ can turn on a rule that is off by default, or change the severity of one that is on.
78
145
 
79
146
  ```json
80
147
  "rules": [
81
148
  {
82
- "files": ["papers/agenticdev-2026/**"],
83
- "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
149
+ "files": ["papers/**"],
150
+ "rules": { "pdf/body-size": "off" }
84
151
  },
85
152
  {
86
153
  "files": ["papers/old-draft/**"],
87
- "rules": { "paper/typography": "off" }
154
+ "rules": { "paper/section-word": "off" }
88
155
  }
89
156
  ]
90
157
  ```
91
158
 
92
- - **`files` and `ignores` are globs relative to the file that holds the settings** — the
93
- directory of your `package.json` — exactly as ESLint resolves them relative to its config file,
159
+ - **`files` and `ignores` are globs relative to the file that holds them** — the project root for
160
+ the root file, the paper for a paper's — exactly as ESLint resolves them relative to its config file,
94
161
  whatever directory you run `paperlint lint` from. A block without `files` applies to every linted file.
95
162
  A pattern ending in `/**` is the usual way to name one paper.
163
+ - **Each rule reaches only the files it is written for.** A block with `"files": ["papers/**"]` and
164
+ `"rules": { "paper/source": "warn" }` turns `paper/source` on for every `PIPELINE-STATUS.md` under
165
+ `papers/`; `paper/section-word` in the same block lands on every `paper.md`, `draft.md` and
166
+ `paper.tex`. You do not need to know which file a rule reads: your `files` narrow where it runs,
167
+ never widen it. The [rule tables](rules.md) name each rule's file.
96
168
  - **A rule entry** is a severity (`"off"`, `"warn"`, `"error"`, or `0`/`1`/`2`), or a list whose
97
169
  first element is a severity and the rest are the rule's options.
98
170
  - **Only rules paperlint ships can be named** — the ones in [`docs/rules.md`](rules.md) and
99
171
  [`docs/optional-rules.md`](optional-rules.md). A rule id paperlint does not ship, a bad severity, a
100
- `rules` that is not a list, or a block key other than `files`, `ignores` and `rules` stops the run
172
+ `rules` of another shape, or a block key other than `files`, `ignores` and `rules` stops the run
101
173
  with a message naming the exact key, before anything is linted.
102
174
  - **An optional rule you turned on must reach a paper.** If no linted `paper.tex` gets the rule —
103
175
  usually a `files` glob with a typo — `paperlint lint` fails and says so: a rule that never runs
@@ -116,7 +188,7 @@ and the real findings leave with it.
116
188
 
117
189
  ```json
118
190
  "structure": {
119
- "markers": ["PIPELINE-STATUS.md", "paper.tex", "paper.md", "venue.json"],
191
+ "markers": ["PIPELINE-STATUS.md", "paper.tex", "paper.md", "paperlint.json"],
120
192
  "require": ["PIPELINE-STATUS.md"],
121
193
  "requireOneOf": [["paper.tex", "paper.md"]],
122
194
  "ignore": []
@@ -173,11 +245,12 @@ papers/my-paper
173
245
 
174
246
  The build does **not** judge the layout. A balanced last page, a page limit, the fonts a venue
175
247
  wants — those are verdicts about the finished PDF, and they belong to lint rules that can be
176
- turned on per venue, given a severity and suppressed with a reason. paperlint once searched for a
248
+ turned on per venue, given a severity and suppressed with a reason: the `pdf/` venue rules
249
+ ([`rules.md`](rules.md#checks-against-the-venue)) and the optional `pdf/last-page-balance`. paperlint once searched for a
177
250
  `\balance` position itself and failed the build when none worked; that was removed on
178
251
  2026-09-24.
179
252
 
180
- The class and its options and the venue in `venue.json` are read from the paper and shown in the
253
+ The class and its options and the venue in `paperlint.json` are read from the paper and shown in the
181
254
  plan; later steps decide from them whether they apply.
182
255
 
183
256
  **`paper.pdf` is deleted before anything runs**, for every targeted paper — before the TeX Live is
@@ -211,7 +284,7 @@ having run on it, because a missing build read as nothing to do.
211
284
 
212
285
  ⚠️ **A `build.sh` or `repro/build-submission.sh` in the paper directory is IGNORED.** Earlier
213
286
  versions ran it; `paperlint build` now says one line — `build.sh is ignored — paperlint builds the paper
214
- itself` — and builds the paper itself. The `buildScripts` key is ignored the same way.
287
+ itself` — and builds the paper itself.
215
288
  Why: [#59](https://github.com/zernie/paperlint/issues/59).
216
289
 
217
290
  ## Using the rules from an existing ESLint config
@@ -225,7 +298,9 @@ the exact config the CLI uses, so the shortest path is:
225
298
  // eslint.config.mjs
226
299
  import { buildConfig } from "paperlint/bin/paperlint.mjs";
227
300
  import { texLanguage } from "paperlint/eslint-rules/latex-language.mjs";
228
- export default buildConfig({ minFindings: 3 }, texLanguage);
301
+ export default buildConfig({}, texLanguage);
229
302
  ```
230
303
 
231
- Then point the CI action's `config` input at that file.
304
+ That config is the whole config for your papers: it starts with a global ignore of every file its
305
+ rules are not written for, so `eslint .` with it lints only the paper files. Do not spread it into a
306
+ config that also lints your JavaScript — that code would be ignored.
package/docs/e2e.md CHANGED
@@ -35,7 +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`
38
+ - `paperlint init` takes the default papers directory and writes no `paperlint.json` for it
39
39
  - `paperlint init` finishes with zero — its doctor found no discrepancy
40
40
  - `paperlint init` wires the hooks into `.claude/settings.json` — the same commands `hooks.json`
41
41
  publishes, once each — and says they need `npm install` in a fresh clone; a second `init`
package/docs/install.md CHANGED
@@ -14,21 +14,6 @@ npx paperlint init
14
14
  Two commands in one terminal, in this order: `init` links the skills and hooks to the copy in
15
15
  your project's `node_modules`, so it needs the install first.
16
16
 
17
- **Upgrading from `research-paper-pipeline`** (the package's name before 2.0.0):
18
- `npm rm research-paper-pipeline && npm i -D paperlint`, then `npx paperlint init`. It moves the
19
- `"research-paper-pipeline"` key in `package.json` to `"paperlint"`, and replaces the hook commands
20
- and skill links that pointed into the old package. Until then the old key is still read, with a
21
- warning.
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
-
32
17
  The npm package carries everything: the `paperlint` command, the ESLint rules, the Claude Code skills
33
18
  and hooks, and the scripts the skills run. External programs are separate:
34
19
 
@@ -43,15 +28,18 @@ and hooks, and the scripts the skills run. External programs are separate:
43
28
 
44
29
  In order:
45
30
 
46
- 1. **Finds the papers directory**, or asks for it. The project must have a `package.json`; without
47
- one `init` stops and tells you to run `npm init -y` first.
48
- 2. **Declares it once**, as the `paperlint` key in your `package.json`.
31
+ 1. **Finds the papers directory**, or asks for it when several directories look like one.
32
+ 2. **Declares it**, in a root `paperlint.json`, only when it is not the default `papers`. A project
33
+ on the defaults gets no config file at all.
49
34
  3. **Links each shipped skill** into `.claude/skills/<name>`, where Claude Code looks for skills.
50
35
  4. **Writes the hook commands** into `.claude/settings.json`, beside your own entries.
51
36
  5. **Offers a GitHub Actions workflow**, pinned to the release tag of the version you installed.
52
37
  6. **Offers a first paper** (`paperlint new`) if the papers directory has none.
53
- 7. **Reports missing external programs** and the command that installs each. It installs nothing.
54
- 8. **Runs `paperlint doctor`** and exits with its verdict.
38
+ 7. **Offers TeX Live** for `paperlint build` (`paperlint toolchain`, ~270 MB, ~3 min, once), with
39
+ the size in the question and NO as the default. Without a terminal it asks nothing and lists the
40
+ command as the next step. `paperlint lint` needs no TeX.
41
+ 8. **Reports missing external programs** and the command that installs each. It installs nothing.
42
+ 9. **Runs `paperlint doctor`** and exits with its verdict.
55
43
 
56
44
  It asks only what it cannot guess or what costs something. A human at a terminal is asked; an
57
45
  agent, CI or `--yes` takes the defaults, and `init` prints which default it took. An unanswered
@@ -61,23 +49,23 @@ question (Ctrl+D) also takes the default.
61
49
 
62
50
  | what | where | when |
63
51
  | ------------------------------------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
64
- | a `paperlint` key naming your papers directory | your `package.json` | always |
52
+ | `{ "papersDir": … }` naming your papers directory | `paperlint.json` | only when it is not the default `papers` |
65
53
  | the three hook commands, merged in beside your own entries | `.claude/settings.json` | by default. A human at a terminal is asked [Y/n]; an agent, CI or `--yes` gets YES; `--no-hooks` skips. Hand-wired under another spelling: nothing written, so nothing runs twice |
66
54
  | a GitHub Actions workflow, pinned to `@v<installed version>` | `.github/workflows/` | only if you say yes; it asks once, and only when a human is at a terminal (stdin and stdout, no `CI`, no `--yes`) |
67
55
  | a first paper, via `paperlint new` | `<papers>/<name>/` | only when the papers directory holds none: asked of a human at a terminal, otherwise only with `--paper <name>` |
68
56
  | one relative symlink per shipped skill, into the installed package | `.claude/skills/<name>` | always — except where that name is already taken (a directory, a file, a link elsewhere): that entry is left as it is and named in the report |
69
57
 
70
- It installs no software and touches nothing else. Commit `.claude/settings.json` so every clone
58
+ It installs no software you did not say yes to, and touches nothing else. Commit `.claude/settings.json` so every clone
71
59
  gets the hooks; the hook commands run files inside `node_modules`, so a fresh clone needs
72
60
  `npm install` first.
73
61
 
74
62
  ## Why it is shaped this way
75
63
 
76
- **One declaration, in `package.json`.** A hook cannot import code or walk up a tree looking for a
77
- config; it can only read a path it can spell, and the one it can always spell is
78
- `$CLAUDE_PROJECT_DIR/package.json`. That key is read by the three hooks, `eslint-rules/papers.mjs`,
79
- `lib/skill-trigger-cases.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`; a separate
80
- config file would be read only by the CLI, so there is none.
64
+ **One file, at the project root.** A hook cannot import code or walk up a tree looking for a
65
+ config; it can only read a path it can spell, and `$CLAUDE_PROJECT_DIR/paperlint.json` is one. The
66
+ same file is read by the CLI, the three hooks, `eslint-rules/papers.mjs`,
67
+ `lib/skill-trigger-cases.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`. It is optional:
68
+ absent, every reader takes the same defaults.
81
69
 
82
70
  **Nothing runs at install time.** No postinstall script and no automatic TeX download. npm's rule
83
71
  is that _"the only valid use of install or preinstall scripts is for compilation"_; husky removed
@@ -110,9 +98,7 @@ resolve.
110
98
  nothing while guarding and says nothing while watching a directory that does not exist, so
111
99
  "installed" and "protecting you" look the same from outside. `doctor` prints the papers directory
112
100
  `paperlint lint` resolves and the one the hooks resolve, whether they match, whether the hooks are wired
113
- (once, twice, or not at all), and which external programs are missing. It also warns when the
114
- project still enables the old `research-paper-pipeline` plugin, because plugin plus settings would
115
- run every hook twice. A plugin installed at user scope is outside what it can read, and it says so.
101
+ (once, twice, or not at all), and which external programs are missing.
116
102
 
117
103
  ### How comparable tools install
118
104
 
@@ -166,13 +152,12 @@ Neither `paperlint lint` nor any of the three hooks loads `@ast-grep` or `typesc
166
152
 
167
153
  ## Troubleshooting
168
154
 
169
- | symptom | cause and fix |
170
- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
171
- | `init`: skills "nothing linked", or hooks that fail to start | the package is not installed in this project (`npx` ran a temporary copy). Run `npm i -D paperlint`, then `npx paperlint init` again |
172
- | `init` stops: no `package.json` | run `npm init -y`, then `npx paperlint init` again |
173
- | `paperlint lint`: `nothing to lint` | no declaration was found. Run `npx paperlint init`, or pass the directory: `paperlint lint papers` |
174
- | hooks fail with `Cannot find module` in a fresh clone | the hook commands run files in `node_modules`: run `npm install` |
175
- | `paperlint: not built` | installed from git with `--ignore-scripts`, or a clone before building: run `npm run build` in the package |
176
- | every hook runs twice | the project still enables the old plugin. `init` and `doctor` print the uninstall command; also remove it from `enabledPlugins` |
177
- | a skill does not show up in Claude Code | its name was already taken in `.claude/skills/`. `init` names it and leaves it alone; rename or remove yours and run `npx paperlint init` again |
178
- | anything else | `npx paperlint doctor` — it checks the setup and exits non-zero on anything miswired |
155
+ | symptom | cause and fix |
156
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | `init`: skills "nothing linked", or hooks that fail to start | the package is not installed in this project (`npx` ran a temporary copy). Run `npm i -D paperlint`, then `npx paperlint init` again |
158
+ | `paperlint lint`: `no papers in papers/` | there is no paper where `papersDir` points. Create one with `npx paperlint new <name>`, or set `"papersDir"` in `paperlint.json` |
159
+ | hooks fail with `Cannot find module` in a fresh clone | the hook commands run files in `node_modules`: run `npm install` |
160
+ | `paperlint: not built` | installed from git with `--ignore-scripts`, or a clone before building: run `npm run build` in the package |
161
+ | a skill does not show up in Claude Code | its name was already taken in `.claude/skills/`. `init` names it and leaves it alone; rename or remove yours and run `npx paperlint init` again |
162
+ | **every** shell command is blocked in Claude Code | `paper-edit-guard` cannot parse the root `paperlint.json` (merge-conflict markers, a typo), and refuses rather than guess. Fix it with a normal file edit — file edits are not blocked |
163
+ | anything else | `npx paperlint doctor` — it checks the setup and exits non-zero on anything miswired |
@@ -1,8 +1,12 @@
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#a-papers-paperlintjson)). No glob is involved: a paper's
7
+ rules apply to that paper. The root `paperlint.json` can turn it on for every paper, or for some
8
+ with a block ([`configuration.md`](configuration.md#the-rules-key-turning-rules-on-and-off)); a
9
+ paper's own file still has the last word for that paper.
6
10
 
7
11
  | rule | what it checks | who needs it |
8
12
  | ----------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
@@ -12,21 +16,18 @@ papers that need them, in the `rules` setting of your `package.json`
12
16
 
13
17
  ### Turning it on
14
18
 
19
+ A paper that extends `paperlint:agenticdev` has it already, at 120 pt. For any other paper, in its
20
+ `papers/my-paper/paperlint.json`:
21
+
15
22
  ```json
16
23
  {
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
- }
24
+ "extends": "paperlint:aisec",
25
+ "kind": "research",
26
+ "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] }
26
27
  }
27
28
  ```
28
29
 
29
- `files` is relative to the `package.json`. `tolerancePt` is how far apart, in points, the two
30
+ 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
31
  columns may end; it defaults to 120. On a real accepted paper the balanced build ended 2.7 pt apart
31
32
  and the one the publisher sent back 321.4 pt apart — nothing in between — so the default leaves a
32
33
  wide margin on both sides.
@@ -38,17 +39,18 @@ the result to `<paper>/_build/paper.facts.json` ([`configuration.md`](configurat
38
39
  and the rule judges that file, reporting on the paper's `paper.tex` at the `\documentclass` line.
39
40
  So: **build, then lint.**
40
41
 
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.
42
+ | the rule finds | it says |
43
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
44
+ | columns further apart than `tolerancePt` | **the finding**, with both heights and how to fix it (below) |
45
+ | 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 |
46
+ | facts about a different PDF than the one on disk | the facts are stale (their SHA-256 differs) — rebuild |
47
+ | facts whose PDF is gone (a failed build removes it) | rebuild |
48
+ | a last page of a few lines | nothing — there is no layout to balance |
49
+ | 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 |
50
+
51
+ If you turn the rule on — in a `paperlint.json` beside no `paper.tex`, or with a `files` glob in
52
+ the root `paperlint.json` that reaches none — `paperlint lint` fails and says so, rather than reporting a clean
53
+ run for a rule that never ran.
52
54
 
53
55
  ### Which venues need it
54
56