paperlint 4.2.0 → 6.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 (251) hide show
  1. package/CLAUDE.md +15 -2
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +23 -12
  4. package/action.harness.mjs +54 -36
  5. package/action.yml +12 -27
  6. package/bin/paperlint.mjs +5 -1
  7. package/dist/adapters/latex/includes.d.ts +5 -1
  8. package/dist/adapters/latex/includes.d.ts.map +1 -1
  9. package/dist/adapters/latex/includes.js +8 -2
  10. package/dist/adapters/latex/includes.js.map +1 -1
  11. package/dist/adapters/latex/index.d.ts +1 -0
  12. package/dist/adapters/latex/index.d.ts.map +1 -1
  13. package/dist/adapters/latex/index.js +3 -0
  14. package/dist/adapters/latex/index.js.map +1 -1
  15. package/dist/adapters/latex/layout.d.ts +27 -0
  16. package/dist/adapters/latex/layout.d.ts.map +1 -0
  17. package/dist/adapters/latex/layout.js +62 -0
  18. package/dist/adapters/latex/layout.js.map +1 -0
  19. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  20. package/dist/adapters/latex/nodes.js +11 -1
  21. package/dist/adapters/latex/nodes.js.map +1 -1
  22. package/dist/adapters/latex/parse.d.ts.map +1 -1
  23. package/dist/adapters/latex/parse.js +5 -1
  24. package/dist/adapters/latex/parse.js.map +1 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +7 -3
  27. package/dist/build.js.map +1 -1
  28. package/dist/cli.d.ts +2 -8
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +92 -42
  31. package/dist/cli.js.map +1 -1
  32. package/dist/domain/anonymity.d.ts +68 -0
  33. package/dist/domain/anonymity.d.ts.map +1 -0
  34. package/dist/domain/anonymity.js +94 -0
  35. package/dist/domain/anonymity.js.map +1 -0
  36. package/dist/domain/body-pages.d.ts +69 -0
  37. package/dist/domain/body-pages.d.ts.map +1 -0
  38. package/dist/domain/body-pages.js +141 -0
  39. package/dist/domain/body-pages.js.map +1 -0
  40. package/dist/domain/paper-source.js +2 -2
  41. package/dist/domain/tex-document.d.ts +9 -0
  42. package/dist/domain/tex-document.d.ts.map +1 -1
  43. package/dist/eslint-rules/latex-language.d.ts +5 -1
  44. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  45. package/dist/eslint-rules/latex-language.js +6 -3
  46. package/dist/eslint-rules/latex-language.js.map +1 -1
  47. package/dist/eslint-rules/paper-context.d.ts +20 -0
  48. package/dist/eslint-rules/paper-context.d.ts.map +1 -0
  49. package/dist/eslint-rules/paper-context.js +44 -0
  50. package/dist/eslint-rules/paper-context.js.map +1 -0
  51. package/dist/eslint-rules/paper-typography.d.ts +1 -1
  52. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  53. package/dist/eslint-rules/paper-typography.js +32 -18
  54. package/dist/eslint-rules/paper-typography.js.map +1 -1
  55. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  56. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  57. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  58. package/dist/eslint-rules/tex-build.js +7 -1
  59. package/dist/eslint-rules/tex-build.js.map +1 -1
  60. package/dist/facts-file.d.ts +28 -2
  61. package/dist/facts-file.d.ts.map +1 -1
  62. package/dist/facts-file.js +54 -9
  63. package/dist/facts-file.js.map +1 -1
  64. package/dist/init.d.ts +4 -19
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +24 -29
  67. package/dist/init.js.map +1 -1
  68. package/dist/lib/paper-config.d.ts.map +1 -1
  69. package/dist/lib/paper-config.js +2 -1
  70. package/dist/lib/paper-config.js.map +1 -1
  71. package/dist/package-dirs.d.ts +2 -0
  72. package/dist/package-dirs.d.ts.map +1 -1
  73. package/dist/package-dirs.js +8 -0
  74. package/dist/package-dirs.js.map +1 -1
  75. package/dist/paper-files.d.ts +2 -2
  76. package/dist/paper-includes.d.ts +17 -0
  77. package/dist/paper-includes.d.ts.map +1 -0
  78. package/dist/paper-includes.js +51 -0
  79. package/dist/paper-includes.js.map +1 -0
  80. package/dist/paper-settings.d.ts +11 -0
  81. package/dist/paper-settings.d.ts.map +1 -1
  82. package/dist/paper-settings.js +38 -4
  83. package/dist/paper-settings.js.map +1 -1
  84. package/dist/pdf-content.d.ts +40 -0
  85. package/dist/pdf-content.d.ts.map +1 -0
  86. package/dist/pdf-content.js +97 -0
  87. package/dist/pdf-content.js.map +1 -0
  88. package/dist/pdf-facts.d.ts +30 -0
  89. package/dist/pdf-facts.d.ts.map +1 -1
  90. package/dist/pdf-facts.js +37 -1
  91. package/dist/pdf-facts.js.map +1 -1
  92. package/dist/ports/latex.d.ts +3 -1
  93. package/dist/ports/latex.d.ts.map +1 -1
  94. package/dist/presets.d.ts +2 -0
  95. package/dist/presets.d.ts.map +1 -1
  96. package/dist/presets.js +4 -0
  97. package/dist/presets.js.map +1 -1
  98. package/dist/rules-config.d.ts +1 -0
  99. package/dist/rules-config.d.ts.map +1 -1
  100. package/dist/rules-config.js.map +1 -1
  101. package/dist/structure.js +1 -1
  102. package/dist/structure.js.map +1 -1
  103. package/dist/tex-paper.d.ts +17 -3
  104. package/dist/tex-paper.d.ts.map +1 -1
  105. package/dist/tex-paper.js +36 -38
  106. package/dist/tex-paper.js.map +1 -1
  107. package/dist/tex-requirements.d.ts +4 -0
  108. package/dist/tex-requirements.d.ts.map +1 -1
  109. package/dist/tex-requirements.js +3 -0
  110. package/dist/tex-requirements.js.map +1 -1
  111. package/dist/tex-venue-rules.d.ts +7 -1
  112. package/dist/tex-venue-rules.d.ts.map +1 -1
  113. package/dist/tex-venue-rules.js +40 -0
  114. package/dist/tex-venue-rules.js.map +1 -1
  115. package/dist/types.d.ts +2 -0
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/venue-rules.d.ts +38 -8
  118. package/dist/venue-rules.d.ts.map +1 -1
  119. package/dist/venue-rules.js +179 -41
  120. package/dist/venue-rules.js.map +1 -1
  121. package/docs/configuration.md +23 -20
  122. package/docs/e2e.md +32 -11
  123. package/docs/install.md +8 -8
  124. package/docs/prior-art/paper-folder-scaffolding.md +4 -7
  125. package/docs/rules/anonymity/identity.md +111 -0
  126. package/docs/rules/format/layout-override.md +61 -0
  127. package/docs/rules/format/page-limit.md +108 -0
  128. package/docs/rules/pdf/body-size.md +87 -0
  129. package/docs/rules.md +55 -46
  130. package/eslint-rules/latex-language.ts +10 -4
  131. package/eslint-rules/paper-context.test.ts +47 -0
  132. package/eslint-rules/paper-context.ts +57 -0
  133. package/eslint-rules/paper-typography.ts +67 -23
  134. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  135. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  136. package/eslint-rules/tex-build.ts +8 -1
  137. package/fixtures/build-e2e/aidc/expect.json +1 -1
  138. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  139. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  140. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  141. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  142. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  143. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  144. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  145. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  146. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  147. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  148. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  149. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  150. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  151. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  152. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  153. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  154. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  155. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  156. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  157. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  158. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  159. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  160. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  161. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  162. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  163. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  164. package/fixtures/real-markdown-paper/baseline.json +1 -2
  165. package/lib/paper-config.ts +3 -1
  166. package/package.json +1 -1
  167. package/presets/acm-sigconf.jsonc +3 -1
  168. package/presets/aidc.jsonc +16 -17
  169. package/presets/ieee-conference.jsonc +8 -0
  170. package/presets/venue-profile.schema.json +9 -0
  171. package/scripts/check.ts +2 -2
  172. package/scripts/eslint-report-guard.mjs +5 -2
  173. package/scripts/harness-api.frozen.json +0 -1
  174. package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
  175. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  176. package/src/adapters/latex/includes.ts +10 -2
  177. package/src/adapters/latex/index.test.ts +8 -0
  178. package/src/adapters/latex/index.ts +3 -0
  179. package/src/adapters/latex/layout.ts +106 -0
  180. package/src/adapters/latex/nodes.ts +11 -3
  181. package/src/adapters/latex/parse.ts +5 -1
  182. package/src/build.harness.mjs +6 -4
  183. package/src/build.test.ts +23 -1
  184. package/src/build.ts +7 -3
  185. package/src/cli.harness.mjs +22 -54
  186. package/src/cli.test.ts +15 -18
  187. package/src/cli.ts +114 -46
  188. package/src/domain/anonymity.test.ts +163 -0
  189. package/src/domain/anonymity.ts +129 -0
  190. package/src/domain/body-pages.test.ts +300 -0
  191. package/src/domain/body-pages.ts +223 -0
  192. package/src/domain/paper-source.ts +2 -2
  193. package/src/domain/tex-document.ts +10 -0
  194. package/src/facts-file.test.ts +100 -4
  195. package/src/facts-file.ts +91 -9
  196. package/src/init.test.ts +33 -13
  197. package/src/init.ts +25 -48
  198. package/src/linted-set.test.ts +11 -1
  199. package/src/new-paper.harness.mjs +1 -5
  200. package/src/new-paper.test.ts +7 -1
  201. package/src/package-dirs.ts +9 -0
  202. package/src/paper-files.ts +2 -2
  203. package/src/paper-includes.test.ts +161 -0
  204. package/src/paper-includes.ts +77 -0
  205. package/src/paper-settings.test.ts +63 -6
  206. package/src/paper-settings.ts +53 -5
  207. package/src/pdf-content.test.ts +71 -0
  208. package/src/pdf-content.ts +125 -0
  209. package/src/pdf-facts.test.ts +110 -2
  210. package/src/pdf-facts.ts +106 -8
  211. package/src/ports/latex.ts +3 -0
  212. package/src/presets.ts +8 -8
  213. package/src/rule-cases.test.ts +29 -33
  214. package/src/rule-docs.test.ts +0 -3
  215. package/src/rules-config.ts +1 -0
  216. package/src/structure.harness.ts +1 -3
  217. package/src/structure.ts +1 -1
  218. package/src/tex-paper.test.ts +78 -61
  219. package/src/tex-paper.ts +57 -48
  220. package/src/tex-requirements.ts +9 -0
  221. package/src/tex-venue-rules.test.ts +111 -6
  222. package/src/tex-venue-rules.ts +57 -2
  223. package/src/types.ts +2 -0
  224. package/src/venue-rules.test.ts +351 -39
  225. package/src/venue-rules.ts +247 -47
  226. package/templates/paper/PIPELINE-STATUS.md +0 -4
  227. package/dist/action-ref.d.ts +0 -12
  228. package/dist/action-ref.d.ts.map +0 -1
  229. package/dist/action-ref.js +0 -16
  230. package/dist/action-ref.js.map +0 -1
  231. package/dist/eslint-rules/paper-research-question.d.ts +0 -31
  232. package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
  233. package/dist/eslint-rules/paper-research-question.js +0 -188
  234. package/dist/eslint-rules/paper-research-question.js.map +0 -1
  235. package/docs/rules/tex/missing-input.md +0 -58
  236. package/eslint-rules/paper-research-question.harness.mjs +0 -212
  237. package/eslint-rules/paper-research-question.ts +0 -199
  238. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
  239. package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
  240. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
  241. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
  242. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
  243. package/fixtures/paper-research-question/draft/paper.tex +0 -2
  244. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
  245. package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
  246. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
  247. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
  248. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
  249. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
  250. package/src/action-ref.test.ts +0 -26
  251. package/src/action-ref.ts +0 -15
package/docs/rules.md CHANGED
@@ -3,36 +3,43 @@
3
3
  The README lists what each check catches in one line. This page says exactly which file each one
4
4
  reads and when it fails. Errors fail `paperlint lint`; warnings print and do not.
5
5
 
6
- | Rule | Level | Reads | Fails when |
7
- | ------------------------------------------------------- | ----- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
- | `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares |
9
- | `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it) |
10
- | `paper/author-list` | error | `paper.tex` → `_build/references.json` | an entry's authors are not those of the version it cites — typically the arXiv preprint's list under a published venue. Checked online by `paperlint build` (DBLP), reported on the entry's own line |
11
- | `paper/research-question` | warn | `paper.tex`, `paper.md`, `PIPELINE-STATUS.md` | a stage is declared and either the scorecard has no `researchQuestion` field, or it has one the paper does not contain. The declared sentence is compared against the source with whitespace collapsed — your words, not a pattern. Advisory, because nothing here can judge whether what you declared _is_ a research question |
12
- | `paper/section-word` | warn | `paper.tex`, `paper.md` | `§` or `\S\ref` instead of the word Section. **Fixable:** `--fix` writes `Section~\ref{…}` / `Section 5` |
13
- | `paper/leading-zero` | warn | `paper.tex`, `paper.md` | a decimal below 1 without its leading zero (`.05` instead of `0.05`, IEEE / ISO 80000-1 style), counted only where the reader sees it — prose, math and table cells, not macro options, column specs, comments, code or tikz. A number right after `¶` or `§` (`¶¶.42`) is a paragraph designator and is not reported; in a paragraph that cites numbers that way, another bare decimal is reported without a fix, with the zero as a suggestion. **Fixable** |
14
- | `paper/figure-ref-style` | warn | `paper.tex` | `Fig.~\ref` and `Figure~\ref` mixed in one document; each minority occurrence is reported. **Fixable:** `--fix` writes the majority form |
15
- | `bib/reachable-entry` | warn | `paper.tex` (its `filecontents` bibliography) | an entry has no doi, url or arXiv id — a reader has nothing to follow. Reported on the entry; no fix, the link has to be looked up |
16
- | `paper/cite-exists` | error | `paper.tex` → `_build/references.json` | an entry's doi or arXiv id resolves to nothing, or to a different work. Checked online by `paperlint build`; a work that was merely not found is not a finding |
17
- | `paper/refs-fresh` | error | `paper.tex` → `_build/references.json` | the bibliography changed since the build checked it (its SHA-256 differs) — run `npx paperlint build` |
18
- | `paper/refs-checked` | warn | `paper.tex` → `_build/references.json` | the paper has a bibliography and no build has checked it, or the last build could not (no network) — so the two rules above did not run |
19
- | `tex/future-promise` | warn | `paper.tex` | a camera-ready build still says "will be released" about something already handed over |
20
- | `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
21
- | [`tex/template`](rules/tex/template.md) | error | `paper.tex` → `paperlint.json` | the `\documentclass` is not the preset's `template` class, or lacks an option it names — [page](rules/tex/template.md) |
22
- | [`tex/required-section`](rules/tex/required-section.md) | error | `paper.tex` → `paperlint.json` | a section the preset requires (AIDC: «LLM Usage Statement», last) is missing or not where it must be — [page](rules/tex/required-section.md) |
23
- | [`tex/venue-leftover`](rules/tex/venue-leftover.md) | warn | `paper.tex` → `paperlint.json` | the text names another shipped venue than the one the paper extends («Submission to VenueA 2026» in a paper now set for VenueB) — [page](rules/tex/venue-leftover.md) |
24
- | [`tex/claim-provenance`](rules/tex/claim-provenance.md) | warn | `paper.tex` | a sentence of the body states a number and does not say whose it is — no citation or link, no _we_/_our_, no `\ref` or "Table 2", no sample ("n = 134", "48 runs") — [page](rules/tex/claim-provenance.md) |
25
- | [`tex/missing-input`](rules/tex/missing-input.md) | warn | `paper.tex` and the files it includes | an `\input`, `\include` or `\subfile` names a file that is not there, so no rule read its text — [page](rules/tex/missing-input.md) |
26
- | [`tex/register`](rules/tex/register.md) | warn | `paper.tex` and the files it includes | the body runs to a post's register: more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or _Yet_ — the limit is an option — [page](rules/tex/register.md) |
27
- | `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
28
- | `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
29
- | `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) |
30
- | `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, 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 |
31
- | `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`) |
32
- | `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 |
33
- | `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` |
34
- | `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) |
35
- | `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 |
6
+ | Rule | Level | Reads | Fails when |
7
+ | ----------------------------------------------------------- | ----- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares |
9
+ | `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it) |
10
+ | `paper/author-list` | error | `paper.tex` → `_build/references.json` | an entry's authors are not those of the version it cites — typically the arXiv preprint's list under a published venue. Checked online by `paperlint build` (DBLP), reported on the entry's own line |
11
+ | `paper/section-word` | warn | `paper.tex` and its body files, `paper.md` | `§` or `\S\ref` instead of the word Section. **Fixable:** `--fix` writes `Section~\ref{…}` / `Section 5` |
12
+ | `paper/leading-zero` | warn | `paper.tex` and its body files, `paper.md` | a decimal below 1 without its leading zero (`.05` instead of `0.05`, IEEE / ISO 80000-1 style), counted only where the reader sees it — prose, math and table cells, not macro options, column specs, comments, code or tikz. A number right after `¶` or `§` (`¶¶.42`) is a paragraph designator and is not reported; in a paragraph that cites numbers that way, another bare decimal is reported without a fix, with the zero as a suggestion. **Fixable** |
13
+ | `paper/figure-ref-style` | warn | `paper.tex` and its body files | `Fig.~\ref` and `Figure~\ref` mixed in one document; each minority occurrence is reported. **Fixable:** `--fix` writes the majority form |
14
+ | `bib/reachable-entry` | warn | `paper.tex` (its `filecontents` bibliography) | an entry has no doi, url or arXiv id — a reader has nothing to follow. Reported on the entry; no fix, the link has to be looked up |
15
+ | `paper/cite-exists` | error | `paper.tex` → `_build/references.json` | an entry's doi or arXiv id resolves to nothing, or to a different work. Checked online by `paperlint build`; a work that was merely not found is not a finding |
16
+ | `paper/refs-fresh` | error | `paper.tex` → `_build/references.json` | the bibliography changed since the build checked it (its SHA-256 differs) — run `npx paperlint build` |
17
+ | `paper/refs-checked` | warn | `paper.tex` → `_build/references.json` | the paper has a bibliography and no build has checked it, or the last build could not (no network) — so the two rules above did not run |
18
+ | `tex/future-promise` | warn | `paper.tex` and its body files | a camera-ready build still says "will be released" about something already handed over |
19
+ | `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
20
+ | [`tex/template`](rules/tex/template.md) | error | `paper.tex` → `paperlint.json` | the `\documentclass` is not the preset's `template` class, or lacks an option it names — [page](rules/tex/template.md) |
21
+ | [`tex/required-section`](rules/tex/required-section.md) | error | `paper.tex` → `paperlint.json` | a section the preset requires (AIDC: «LLM Usage Statement», last) is missing or not where it must be — [page](rules/tex/required-section.md) |
22
+ | [`format/layout-override`](rules/format/layout-override.md) | error | `paper.tex` → `paperlint.json` | the source changes the page layout the venue's template sets for the whole document — `geometry`, a layout `\setlength`, `\linespread`, `\baselinestretch` — [page](rules/format/layout-override.md) |
23
+ | [`anonymity/identity`](rules/anonymity/identity.md) | error | `paper.tex` → `_build/paper.facts.json` | a double-blind venue (`"blind": true`) and no `identity` declared, or a declared name, handle, email or project in the PDF's text, metadata or link targets — [page](rules/anonymity/identity.md) |
24
+ | [`tex/venue-leftover`](rules/tex/venue-leftover.md) | warn | `paper.tex` → `paperlint.json` | the text names another shipped venue than the one the paper extends («Submission to VenueA 2026» in a paper now set for VenueB) — [page](rules/tex/venue-leftover.md) |
25
+ | [`tex/claim-provenance`](rules/tex/claim-provenance.md) | warn | `paper.tex` | a sentence of the body states a number and does not say whose it is — no citation or link, no _we_/_our_, no `\ref` or "Table 2", no sample ("n = 134", "48 runs") — [page](rules/tex/claim-provenance.md) |
26
+ | [`tex/register`](rules/tex/register.md) | warn | `paper.tex` and the files it includes | the body runs to a post's register: more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or _Yet_ — the limit is an option — [page](rules/tex/register.md) |
27
+ | `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
28
+ | `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
29
+ | `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 3, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
30
+ | `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, 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 |
31
+ | `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`) |
32
+ | `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 |
33
+ | [`format/page-limit`](rules/format/page-limit.md) | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limits of the paper's kind. With `body_ends_at: "references"` (AIDC) the body ends at the references or the appendix, whichever comes first — found by hyperref's anchors, then the heading line — and the page it ends on counts when body text stands above the heading; an end it cannot find, or signals that disagree, are reported — [page](rules/format/page-limit.md) |
34
+ | [`pdf/body-size`](rules/pdf/body-size.md) | error | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's, or the reference font size is outside the preset's range widened by `body_pt_tol`. banal reads 0.3 pt above the size the glyphs are set in; checked on five built accepted papers (none reported) and three variants one size off (each reported) — [page](rules/pdf/body-size.md) |
35
+ | `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 |
36
+
37
+ **Body files** are the files `paper.tex` includes from its document body (`\input`, `\include`,
38
+ `\subfile`, nested ones too), found in the paper's directory — not a preamble include, not a file
39
+ found only in paperlint's own inputs, and not any other `.tex` beside the paper (`versions/`). A
40
+ finding in one is reported at that file and line, and `--fix` edits that file. Which reference form
41
+ is the majority and whether the build is a review build are decided over the whole paper. An include
42
+ that names no file is printed as a note: lint did not read it, and the build will stop on it.
36
43
 
37
44
  `tex/required-section` requires AIDC's «LLM Usage Statement» **always**. ACSAC asks for it «if
38
45
  LLMs are used», which no rule can know; an author who used none says so in the section
@@ -89,14 +96,14 @@ for papers, each with the quote it came from, the TeX packages its template need
89
96
  the venue implies. A preset may itself extend another: the ACM venues extend the `acm-sigconf`
90
97
  family, which holds everything the ACM template decides.
91
98
 
92
- | preset | extends | template | kinds | page limit checked | rules it turns on |
93
- | --------------------------- | --------------------------- | ------------------------------------- | -------------------------------------------------------- | ------------------ | ----------------------- |
94
- | `paperlint:acm-sigconf` | — | ACM `acmart` sigconf | none | no (no kinds) | — |
95
- | `paperlint:agenticdev` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `short` (5 + 2 refs), `full` (10 + 2), `demo` (5 + 2) | yes | `pdf/last-page-balance` |
96
- | `paperlint:aisec` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `research`, `benchmark`, `position`, `sok` (10 + 2 each) | yes | — |
97
- | `paperlint:realm` | — | ACL | `long`, `short` | no — see below | — |
98
- | `paperlint:ieee-conference` | — | IEEE `IEEEtran` conference | none | no (no kinds) | — |
99
- | `paperlint:aidc` | `paperlint:ieee-conference` | IEEE `IEEEtran` conference, `compsoc` | `regular` (12), `short` (6) | no — see below | — |
99
+ | preset | extends | template | kinds | page limit checked | rules it turns on |
100
+ | --------------------------- | --------------------------- | ------------------------------------- | -------------------------------------------------------- | -------------------------------------- | ----------------------- |
101
+ | `paperlint:acm-sigconf` | — | ACM `acmart` sigconf | none | no (no kinds) | — |
102
+ | `paperlint:agenticdev` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `short` (5 + 2 refs), `full` (10 + 2), `demo` (5 + 2) | yes | `pdf/last-page-balance` |
103
+ | `paperlint:aisec` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `research`, `benchmark`, `position`, `sok` (10 + 2 each) | yes | — |
104
+ | `paperlint:realm` | — | ACL | `long`, `short` | no — see below | — |
105
+ | `paperlint:ieee-conference` | — | IEEE `IEEEtran` conference | none | no (no kinds) | — |
106
+ | `paperlint:aidc` | `paperlint:ieee-conference` | IEEE `IEEEtran` conference, `compsoc` | `regular` (12), `short` (6) | yes, before the references — see below | — (`blind`) |
100
107
 
101
108
  That is all that ships today. There is no NeurIPS, USENIX or Springer preset. A paper for an ACM or
102
109
  IEEE conference venue nobody has profiled can extend `paperlint:acm-sigconf` or
@@ -104,9 +111,11 @@ IEEE conference venue nobody has profiled can extend `paperlint:acm-sigconf` or
104
111
  says the page limit is not. For anything else, write your own preset
105
112
  ([below](#writing-your-own-venue-preset)). REALM's preset sets no page limit on purpose: banal
106
113
  counts the Limitations and Ethics sections as body, ACL does not, and a limit on banal's number
107
- would fail a correct paper. AIDC's sets none for the same reason, measured: banal counts a page as
108
- body when an appendix or the LLM Usage Statement sits on it before the bibliography, and AIDC counts
109
- neither, so a correct 12-page paper measured 13. AgenticDev's turns on `pdf/last-page-balance`
114
+ would fail a correct paper. AIDC's limit is checked by another count, for the same reason: banal counts
115
+ a page as body when an appendix or the LLM Usage Statement sits on it, and AIDC counts neither, so a
116
+ correct 12-page paper measured 13. Its preset sets `"body_ends_at": "references"`: the body is the
117
+ pages before the one where the references heading first appears. It is also `"blind": true`, which
118
+ turns on [`anonymity/identity`](rules/anonymity/identity.md). AgenticDev's turns on `pdf/last-page-balance`
110
119
  because its proceedings are produced by Conference Publishing Consulting, which sends back an
111
120
  unbalanced last page; AISec's does not, because nothing in hand says who produces the AISec
112
121
  proceedings.
@@ -199,6 +208,7 @@ The numbers and packages above illustrate the shape; take yours from the venue's
199
208
  | `name` | how messages name the venue; the file name otherwise |
200
209
  | `template` | the `\documentclass` the venue's template uses — a whole line (`\documentclass[conference,compsoc]{IEEEtran}`) or a bare class name; `tex/template` requires the paper's class and every option named here |
201
210
  | `aliases` | other names the venue goes by in a paper's text — its acronym, its parent conference: `["AISec", "ACM CCS"]`. Collected along `extends`, with `name`. Read by `tex/venue-leftover` |
211
+ | `blind` | the venue reviews double-blind — set only where its call says so, with the quote. Turns on `anonymity/identity`; a child's value replaces its parent's |
202
212
  | `required_sections` | sections the venue requires, `[{ "title": "LLM Usage Statement", "position": "last" }]`: the exact title, and `last` when it must close the paper. Read by `tex/required-section`; a child's list replaces its parent's |
203
213
  | `format` | page size, columns, fonts, font sizes, and `kinds` (page limits per kind of paper) |
204
214
  | `tex` | TeX Live packages, each with the files that prove it is installed. Required unless `extends` is set |
@@ -214,10 +224,10 @@ Presets from npm packages are not supported yet.
214
224
 
215
225
  ## The paper is LaTeX
216
226
 
217
- The paper body is `paper.tex`, and it gets four rules of its own: `paper/research-question`,
218
- `paper/typography`, `tex/future-promise` and `tex/acm-frontmatter-override` — plus the `pdf/`
219
- venue rules above, which run on it but judge the files beside it. The scorecard and
220
- the review notes are Markdown files, and the other five rules read those.
227
+ The paper body is `paper.tex`, and it gets three rules of its own: `paper/typography`,
228
+ `tex/future-promise` and `tex/acm-frontmatter-override` — plus the `pdf/` venue rules above, which
229
+ run on it but judge the files beside it. The scorecard and the review notes are Markdown files, and
230
+ the other five rules read those.
221
231
 
222
232
  A Markdown body (`paper.md`, or `draft.md`) is still read today and gets only the two `paper/`
223
233
  rules, which is why they list it above. Markdown papers are deprecated and being removed
@@ -241,7 +251,6 @@ stages:
241
251
  bytes: 305412
242
252
  source: versions/2026-07-22-submitted.tex
243
253
  sourceBytes: 57210
244
- researchQuestion: "Does pruning the state space reduce review cost?"
245
254
  ---
246
255
  ```
247
256
 
@@ -51,9 +51,12 @@
51
51
  * paper. The projection counts differently: the same place gave 1239 words before macro
52
52
  * blanking and 1208 after — a 2.5% discrepancy. The discrepancy is ACCEPTED deliberately;
53
53
  * the thresholds were not recalibrated.
54
- * 4. A MULTI-FILE PAPER IS INVISIBLE. `\input` / `\include` are in OPAQUE — their contents
55
- * are not read. The first `\input{sections/method}` yields a silent zero over all the
56
- * prose it pulls in.
54
+ * 4. ONE FILE AT A TIME. `\input` / `\include` are in OPAQUE: the projection of `paper.tex`
55
+ * does not hold what they pull in. `paperlint lint` hands ESLint each file the paper includes
56
+ * from its body as a file of its own (src/paper-includes.ts), and a file with no `document`
57
+ * environment is projected as body throughout — so an included file is read where it is,
58
+ * and its findings point into it. An ESLint run outside `paperlint lint` reads only the
59
+ * files it was given.
57
60
  * 5. `\appendix` IS NOT IMPLEMENTED and is INEXPRESSIBLE in a length-preserving projection:
58
61
  * `\appendix` is 9 characters, `## Appendix` is 11, the synthesis does not fit. On the
59
62
  * first paper with a real `\appendix`, `appendix-ref` / `appendix-ratio` yield a silent
@@ -209,6 +212,9 @@ export type MdNode =
209
212
  readonly value: string;
210
213
  };
211
214
 
215
+ /** The root of a projection: a node that has children. */
216
+ export type MdRoot = Extract<MdNode, { readonly type: "root" | "strong" }>;
217
+
212
218
  /** An inline-config comment: an `html` node's text and where it sits. */
213
219
  interface ConfigNode {
214
220
  readonly value: string;
@@ -305,7 +311,7 @@ export function texToMdast(
305
311
  {
306
312
  parse = (s: string): TexRoot => getParser().parse(s),
307
313
  }: { parse?: (s: string) => TexRoot } = {},
308
- ): { root: MdNode; text: string } {
314
+ ): { root: MdRoot; text: string } {
309
315
  const ast = parse(src);
310
316
  const chars = src.split("");
311
317
  const blank = (from: number, to: number) => {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The paper a linted file belongs to, as `paperlint lint` hands it in ESLint's settings, and the
3
+ * reading of the paper's other files.
4
+ */
5
+ import { mkdtempSync, realpathSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+ import {
10
+ PAPERLINT_SETTINGS,
11
+ otherTexts,
12
+ paperOf,
13
+ textOnDisk,
14
+ } from "./paper-context.ts";
15
+
16
+ describe("paperOf", () => {
17
+ it("the paper under settings.paperlint.paper", () => {
18
+ const paper = { main: "/p/paper.tex", files: ["/p/paper.tex", "/p/a.tex"] };
19
+ expect(paperOf({ [PAPERLINT_SETTINGS]: { paper } })).toEqual(paper);
20
+ });
21
+
22
+ it("none when lint handed none, or handed something else", () => {
23
+ expect(paperOf({})).toBe(null);
24
+ expect(paperOf({ [PAPERLINT_SETTINGS]: { paper: { main: 1 } } })).toBe(
25
+ null,
26
+ );
27
+ });
28
+ });
29
+
30
+ describe("otherTexts", () => {
31
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ctx-")));
32
+ writeFileSync(join(dir, "a.tex"), "A");
33
+ writeFileSync(join(dir, "b.tex"), "B");
34
+ const paper = {
35
+ main: join(dir, "a.tex"),
36
+ files: [join(dir, "a.tex"), join(dir, "b.tex"), join(dir, "gone.tex")],
37
+ };
38
+
39
+ it("every file of the paper but the linted one; a file that cannot be read holds no text", () => {
40
+ expect(otherTexts(paper, join(dir, "a.tex"))).toEqual(["B", ""]);
41
+ expect(textOnDisk(join(dir, "gone.tex"))).toBe("");
42
+ });
43
+
44
+ it("none without a paper", () => {
45
+ expect(otherTexts(null, join(dir, "a.tex"))).toEqual([]);
46
+ });
47
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * THE PAPER A LINTED FILE BELONGS TO — handed to the rules by `paperlint lint` in ESLint's `settings`.
3
+ *
4
+ * ESLint lints one file at a time. A paper whose body is in `sections/*.tex` is several files, and
5
+ * two questions are about the paper, not the file: which reference form (`Fig.` or `Figure`) the
6
+ * paper uses most, and whether the build `paper.tex` declares is a review build. The CLI knows the
7
+ * files (it resolves the includes the way the build does, `bodyFiles` in src/tex-paper.ts) and puts
8
+ * them under `settings.paperlint.paper`
9
+ * for `paper.tex` and for every file it includes. A rule reads the other files from disk.
10
+ *
11
+ * No setting — a file linted outside `paperlint lint`, or a markdown paper — means a paper of one
12
+ * file: every rule decides on the file it was handed, as it did before.
13
+ */
14
+ import { readFileSync } from "node:fs";
15
+ import { z } from "zod";
16
+
17
+ /** The key under ESLint's `settings`; the paper is at `settings[PAPERLINT_SETTINGS].paper`. */
18
+ export const PAPERLINT_SETTINGS = "paperlint";
19
+
20
+ const PaperSetting = z.object({
21
+ /** The absolute path of the paper's `paper.tex`. */
22
+ main: z.string(),
23
+ /** Absolute paths: `paper.tex`, then every file of its body, each once. */
24
+ files: z.array(z.string()).readonly(),
25
+ });
26
+
27
+ /** The paper a file belongs to: its main file and all of its files. */
28
+ export type PaperOfFile = Readonly<z.infer<typeof PaperSetting>>;
29
+
30
+ const Settings = z.looseObject({ paper: PaperSetting });
31
+
32
+ /** The paper the linted file belongs to, or null when lint handed none. */
33
+ export function paperOf(
34
+ settings: Readonly<Record<string, unknown>>,
35
+ ): PaperOfFile | null {
36
+ const r = Settings.safeParse(settings[PAPERLINT_SETTINGS]);
37
+ return r.success ? r.data.paper : null;
38
+ }
39
+
40
+ /**
41
+ * The text of a file on disk; empty when it cannot be read — a file gone since `paperlint lint`
42
+ * named it holds no text for any rule.
43
+ */
44
+ export function textOnDisk(path: string): string {
45
+ try {
46
+ return readFileSync(path, "utf8");
47
+ } catch {
48
+ return "";
49
+ }
50
+ }
51
+
52
+ /** The texts of the paper's files other than `filename`, read from disk; none without a paper. */
53
+ export const otherTexts = (
54
+ paper: PaperOfFile | null,
55
+ filename: string,
56
+ ): readonly string[] =>
57
+ (paper?.files ?? []).filter((f) => f !== filename).map(textOnDisk);
@@ -63,7 +63,13 @@ import assert from "node:assert/strict";
63
63
  import { getParser } from "@unified-latex/unified-latex-util-parse";
64
64
  import { takeWhile } from "remeda";
65
65
  import type { Nodes } from "mdast";
66
- import type { TexArg, TexNode, TexRoot } from "./latex-language.ts";
66
+ import {
67
+ texToMdast,
68
+ type TexArg,
69
+ type TexNode,
70
+ type TexRoot,
71
+ } from "./latex-language.ts";
72
+ import { otherTexts, paperOf } from "./paper-context.ts";
67
73
  import type {
68
74
  RuleContext,
69
75
  RuleFixer,
@@ -102,16 +108,27 @@ export function bibRange(text: string): BibRange | null {
102
108
  return { start: m.index, end: m.index + whole.length, bodyStart, body };
103
109
  }
104
110
 
111
+ /** A node of a file's projection, as far as its place goes: mdast's, or the `.tex` language's. */
112
+ interface ProjectedNode {
113
+ readonly type: string;
114
+ readonly position?:
115
+ | {
116
+ readonly start: { readonly offset?: number | undefined };
117
+ readonly end: { readonly offset?: number | undefined };
118
+ }
119
+ | undefined;
120
+ }
121
+
105
122
  /**
106
123
  * The ranges a markup rule must not read: LaTeX comments (the language gives them as `html`
107
124
  * nodes, with positions) and the inline bibliography.
108
125
  */
109
126
  function skippedRanges(
110
- sourceCode: RuleSourceCode,
127
+ nodes: readonly ProjectedNode[],
111
128
  raw: string,
112
129
  ): [number, number][] {
113
130
  const out: [number, number][] = [];
114
- for (const n of sourceCode.ast?.children ?? [])
131
+ for (const n of nodes)
115
132
  if (n.type === "html" && n.position) {
116
133
  // An offset-less point compares as NaN: inside no range, as `undefined` always did.
117
134
  const {
@@ -197,12 +214,22 @@ export function crefDefinitionRanges(
197
214
  const inside = (ranges: readonly [number, number][], i: number) =>
198
215
  ranges.some(([s, e]) => i >= s && i < e);
199
216
 
200
- /** Every match of `re` in `raw` outside the skipped ranges. */
201
- function markupMatches(sourceCode: TexSourceCode, re: RegExp) {
202
- const raw = sourceCode.raw;
203
- const skip = skippedRanges(sourceCode, raw);
217
+ /** Every match of `re` in `raw` outside the ranges skipped in it (`nodes`: its projection's). */
218
+ const markupMatchesIn = (
219
+ raw: string,
220
+ nodes: readonly ProjectedNode[],
221
+ re: RegExp,
222
+ ): readonly RegExpExecArray[] => {
223
+ const skip = skippedRanges(nodes, raw);
204
224
  return [...raw.matchAll(re)].filter((m) => !inside(skip, m.index));
205
- }
225
+ };
226
+
227
+ /** Every match of `re` in the linted file outside the skipped ranges. */
228
+ const markupMatches = (
229
+ sourceCode: TexSourceCode,
230
+ re: RegExp,
231
+ ): readonly RegExpExecArray[] =>
232
+ markupMatchesIn(sourceCode.raw, sourceCode.ast?.children ?? [], re);
206
233
 
207
234
  const isTex = (sourceCode: RuleSourceCode): sourceCode is TexSourceCode =>
208
235
  typeof sourceCode.raw === "string";
@@ -510,25 +537,42 @@ function leadingZero(context: RuleContext) {
510
537
 
511
538
  const FIG_REF = /\b(Fig\.|Figure)(?=~?\\(?:ref|autoref)\b)/g;
512
539
 
540
+ /** The reference forms another file of the paper writes, read the way the linted file is. */
541
+ const figureFormsIn = (raw: string): readonly (string | undefined)[] =>
542
+ markupMatchesIn(raw, texToMdast(raw).root.children, FIG_REF).map(
543
+ ([, form]) => form,
544
+ );
545
+
546
+ /**
547
+ * The minority form is rewritten to the majority one; on a tie, to the full word. The majority is
548
+ * the PAPER's: a body split into `sections/*.tex` is one document to its reader, so the other files
549
+ * `paperlint lint` names for this one (`paperOf`) are counted too, and only this file is reported.
550
+ */
513
551
  function figureRefStyle(context: RuleContext) {
514
552
  const sc = context.sourceCode;
515
553
  if (!isTex(sc)) return;
516
- const all = markupMatches(sc, FIG_REF);
517
- const short = all.filter((m) => m[1] === "Fig.");
518
- const long = all.filter((m) => m[1] === "Figure");
519
- if (!short.length || !long.length) return;
520
- // The minority form is rewritten to the majority one; on a tie, to the full word.
521
- const [minority, word] =
522
- short.length > long.length ? [long, "Fig."] : [short, "Figure"];
523
- for (const m of minority) {
524
- const [, form = ""] = m;
525
- const range: [number, number] = [m.index, m.index + form.length];
526
- at(context, range[0], range[1], {
527
- messageId: "mixed",
528
- data: { form, word, n: String(all.length - minority.length) },
529
- fix: (f) => f.replaceTextRange(range, word),
554
+ const own = markupMatches(sc, FIG_REF);
555
+ const forms: readonly (string | undefined)[] = [
556
+ ...own.map(([, form]) => form),
557
+ ...otherTexts(paperOf(context.settings), context.filename).flatMap(
558
+ figureFormsIn,
559
+ ),
560
+ ];
561
+ const short = forms.filter((f) => f === "Fig.").length;
562
+ const long = forms.filter((f) => f === "Figure").length;
563
+ if (short === 0 || long === 0) return;
564
+ const [minority, word, n] =
565
+ short > long ? ["Figure", "Fig.", short] : ["Fig.", "Figure", long];
566
+ own
567
+ .filter(([, form]) => form === minority)
568
+ .forEach((m) => {
569
+ const range = [m.index, m.index + minority.length] as const;
570
+ at(context, range[0], range[1], {
571
+ messageId: "mixed",
572
+ data: { form: minority, word, n: String(n) },
573
+ fix: (f) => f.replaceTextRange(range, word),
574
+ });
530
575
  });
531
- }
532
576
  }
533
577
 
534
578
  const rule = (
@@ -39,7 +39,12 @@ const TEX =
39
39
  const PDF = "%PDF-1.5 pretend";
40
40
  const sha = (s) => createHash("sha256").update(s).digest("hex");
41
41
  const facts = (lastPage, over = {}) => ({
42
- schema: 2,
42
+ schema: 3,
43
+ pages_text: [],
44
+ metadata: {},
45
+ links: [],
46
+ bib_anchor_page: null,
47
+ appendix_anchor_page: null,
43
48
  pdf: "paper.pdf",
44
49
  pdf_sha256: sha(PDF),
45
50
  last_page: lastPage,
@@ -91,7 +91,7 @@ const RuleOptions = z
91
91
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
92
92
  export const DEFAULT_TOLERANCE_PT = 120;
93
93
  /** The schema of `paper.facts.json` this rule reads (`src/facts-file.ts`, FACTS_SCHEMA). */
94
- export const FACTS_SCHEMA = 2;
94
+ export const FACTS_SCHEMA = 3;
95
95
  export const FACTS_REL = join("_build", "paper.facts.json");
96
96
 
97
97
  /** The `last_page` field, parsed. */
@@ -249,7 +249,7 @@ const rule = {
249
249
  factsBroken:
250
250
  "{{why}} in _build/paper.facts.json — rebuild the paper (`paperlint build`) to rewrite it",
251
251
  schema:
252
- "_build/paper.facts.json has schema {{got}}; this rule reads schema 2 — rebuild the paper " +
252
+ "_build/paper.facts.json has schema {{got}}; this rule reads schema 3 — rebuild the paper " +
253
253
  "(`paperlint build`) to rewrite it",
254
254
  pdfMissing:
255
255
  "_build/paper.facts.json describes {{pdf}}, which is not on disk (a failed build removes " +
@@ -51,6 +51,7 @@
51
51
  */
52
52
  import { readFileSync } from "node:fs";
53
53
  import type { RuleContext } from "./rule-context.ts";
54
+ import { paperOf, textOnDisk } from "./paper-context.ts";
54
55
 
55
56
  /**
56
57
  * A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
@@ -169,7 +170,13 @@ const futurePromise = {
169
170
  } catch {
170
171
  return;
171
172
  }
172
- if (REVIEW_MODE_RE.test(text)) return;
173
+ // The build mode is the PAPER's: an included file declares none, `paper.tex` does.
174
+ const main = paperOf(context.settings)?.main;
175
+ const declares =
176
+ main === undefined || main === context.filename
177
+ ? text
178
+ : textOnDisk(main);
179
+ if (REVIEW_MODE_RE.test(declares)) return;
173
180
  // Comments are blanked by CUTTING THE TAIL of the line, exactly as the previous
174
181
  // implementation did. Columns do not shift: a match is only possible to the LEFT of
175
182
  // `%`, and the left part of the line stays in place character for character. An
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "A real IEEEtran [conference,compsoc] paper for AIDC, built on paperlint's TeX Live: IEEE's Times, and not one finding from any rule — which is what judges pdf/limits, pdf/body-size and the rest against a real PDF of the class.",
2
+ "$comment": "A real IEEEtran [conference,compsoc] paper for AIDC, built on paperlint's TeX Live: IEEE's Times, and not one finding from any rule — which is what judges format/page-limit, pdf/body-size and the rest against a real PDF of the class.",
3
3
  "build": {
4
4
  "outcome": "pdf"
5
5
  },
@@ -1 +1 @@
1
- { "extends": "paperlint:aidc", "kind": "short" }
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: B: the name in an acknowledgments paragraph. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "«Ada Example»",
13
+ "PDF, page"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,31 @@
1
+ % B: the name in an acknowledgments paragraph.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \begin{filecontents*}{refs.bib}
5
+ @article{knuth84,
6
+ title = {Literate Programming},
7
+ author = {Knuth, Donald E.},
8
+ journal = {The Computer Journal},
9
+ volume = {27},
10
+ number = {2},
11
+ pages = {97--111},
12
+ year = {1984},
13
+ doi = {10.1093/comjnl/27.2.97}
14
+ }
15
+ \end{filecontents*}
16
+ \begin{document}
17
+ \title{A Fixture for Double-Blind Review}
18
+ \author{\IEEEauthorblockN{Anonymous Authors}}
19
+ \maketitle
20
+ \section{Introduction}
21
+ This paper builds on earlier work~\cite{knuth84}.
22
+
23
+ \section{Conclusion}
24
+ The conclusion restates that the fixture builds.
25
+ \section*{Acknowledgments}
26
+ We thank Ada Example for comments on a draft.
27
+ \bibliographystyle{IEEEtran}
28
+ \bibliography{refs}
29
+ \section*{LLM Usage Statement}
30
+ No LLMs were used in writing this fixture.
31
+ \end{document}