paperlint 5.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CLAUDE.md +13 -0
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +8 -8
  4. package/bin/paperlint.mjs +5 -1
  5. package/dist/adapters/latex/index.d.ts +2 -1
  6. package/dist/adapters/latex/index.d.ts.map +1 -1
  7. package/dist/adapters/latex/index.js +6 -2
  8. package/dist/adapters/latex/index.js.map +1 -1
  9. package/dist/adapters/latex/layout.d.ts +27 -0
  10. package/dist/adapters/latex/layout.d.ts.map +1 -0
  11. package/dist/adapters/latex/layout.js +62 -0
  12. package/dist/adapters/latex/layout.js.map +1 -0
  13. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  14. package/dist/adapters/latex/nodes.js +11 -1
  15. package/dist/adapters/latex/nodes.js.map +1 -1
  16. package/dist/adapters/latex/parse.d.ts.map +1 -1
  17. package/dist/adapters/latex/parse.js +5 -1
  18. package/dist/adapters/latex/parse.js.map +1 -1
  19. package/dist/adapters/latex/prose.d.ts +6 -1
  20. package/dist/adapters/latex/prose.d.ts.map +1 -1
  21. package/dist/adapters/latex/prose.js +142 -31
  22. package/dist/adapters/latex/prose.js.map +1 -1
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +35 -18
  25. package/dist/cli.js.map +1 -1
  26. package/dist/domain/anonymity.d.ts +68 -0
  27. package/dist/domain/anonymity.d.ts.map +1 -0
  28. package/dist/domain/anonymity.js +94 -0
  29. package/dist/domain/anonymity.js.map +1 -0
  30. package/dist/domain/body-pages.d.ts +69 -0
  31. package/dist/domain/body-pages.d.ts.map +1 -0
  32. package/dist/domain/body-pages.js +141 -0
  33. package/dist/domain/body-pages.js.map +1 -0
  34. package/dist/domain/register.d.ts +96 -0
  35. package/dist/domain/register.d.ts.map +1 -0
  36. package/dist/domain/register.js +136 -0
  37. package/dist/domain/register.js.map +1 -0
  38. package/dist/domain/tex-document.d.ts +28 -0
  39. package/dist/domain/tex-document.d.ts.map +1 -1
  40. package/dist/domain/tex-document.js +8 -3
  41. package/dist/domain/tex-document.js.map +1 -1
  42. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  43. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  44. package/dist/facts-file.d.ts +28 -2
  45. package/dist/facts-file.d.ts.map +1 -1
  46. package/dist/facts-file.js +54 -9
  47. package/dist/facts-file.js.map +1 -1
  48. package/dist/lib/paper-config.d.ts.map +1 -1
  49. package/dist/lib/paper-config.js +2 -1
  50. package/dist/lib/paper-config.js.map +1 -1
  51. package/dist/paper-settings.d.ts +11 -0
  52. package/dist/paper-settings.d.ts.map +1 -1
  53. package/dist/paper-settings.js +38 -4
  54. package/dist/paper-settings.js.map +1 -1
  55. package/dist/pdf-content.d.ts +40 -0
  56. package/dist/pdf-content.d.ts.map +1 -0
  57. package/dist/pdf-content.js +97 -0
  58. package/dist/pdf-content.js.map +1 -0
  59. package/dist/pdf-facts.d.ts +30 -0
  60. package/dist/pdf-facts.d.ts.map +1 -1
  61. package/dist/pdf-facts.js +37 -1
  62. package/dist/pdf-facts.js.map +1 -1
  63. package/dist/ports/latex.d.ts +8 -1
  64. package/dist/ports/latex.d.ts.map +1 -1
  65. package/dist/presets.d.ts +5 -0
  66. package/dist/presets.d.ts.map +1 -1
  67. package/dist/presets.js +8 -1
  68. package/dist/presets.js.map +1 -1
  69. package/dist/register-bands.d.ts +37 -0
  70. package/dist/register-bands.d.ts.map +1 -0
  71. package/dist/register-bands.js +196 -0
  72. package/dist/register-bands.js.map +1 -0
  73. package/dist/register.d.ts +4 -2
  74. package/dist/register.d.ts.map +1 -1
  75. package/dist/register.js +6 -45
  76. package/dist/register.js.map +1 -1
  77. package/dist/tex-requirements.d.ts +10 -0
  78. package/dist/tex-requirements.d.ts.map +1 -1
  79. package/dist/tex-requirements.js +29 -6
  80. package/dist/tex-requirements.js.map +1 -1
  81. package/dist/tex-venue-rules.d.ts +8 -2
  82. package/dist/tex-venue-rules.d.ts.map +1 -1
  83. package/dist/tex-venue-rules.js +40 -0
  84. package/dist/tex-venue-rules.js.map +1 -1
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/venue-rules.d.ts +38 -8
  88. package/dist/venue-rules.d.ts.map +1 -1
  89. package/dist/venue-rules.js +179 -41
  90. package/dist/venue-rules.js.map +1 -1
  91. package/docs/configuration.md +18 -16
  92. package/docs/e2e.md +32 -11
  93. package/docs/rules/anonymity/identity.md +111 -0
  94. package/docs/rules/format/layout-override.md +61 -0
  95. package/docs/rules/format/page-limit.md +108 -0
  96. package/docs/rules/pdf/body-size.md +87 -0
  97. package/docs/rules/tex/claim-emphasis.md +71 -0
  98. package/docs/rules/tex/contrast-frames.md +100 -0
  99. package/docs/rules/tex/relation-markers.md +72 -0
  100. package/docs/rules.md +59 -50
  101. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  102. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  103. package/fixtures/build-e2e/aidc/expect.json +1 -1
  104. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  105. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  106. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  107. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  108. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  109. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  110. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  111. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  112. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  113. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  114. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  115. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  116. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  117. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  118. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  119. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  120. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  121. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  122. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  123. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  124. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  125. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  126. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  127. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  128. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  129. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  130. package/lib/paper-config.ts +3 -1
  131. package/package.json +1 -1
  132. package/presets/acm-sigconf.jsonc +3 -1
  133. package/presets/aidc.jsonc +66 -17
  134. package/presets/ieee-conference.jsonc +8 -0
  135. package/presets/venue-profile.schema.json +57 -1
  136. package/skills/grade-paper-writing/SKILL.md +59 -27
  137. package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
  138. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  139. package/src/adapters/latex/index.test.ts +91 -0
  140. package/src/adapters/latex/index.ts +6 -2
  141. package/src/adapters/latex/layout.ts +106 -0
  142. package/src/adapters/latex/nodes.ts +11 -3
  143. package/src/adapters/latex/parse.ts +5 -1
  144. package/src/adapters/latex/prose.ts +188 -58
  145. package/src/build.harness.mjs +2 -2
  146. package/src/build.test.ts +5 -0
  147. package/src/cli.test.ts +13 -0
  148. package/src/cli.ts +50 -17
  149. package/src/domain/anonymity.test.ts +163 -0
  150. package/src/domain/anonymity.ts +129 -0
  151. package/src/domain/body-pages.test.ts +300 -0
  152. package/src/domain/body-pages.ts +223 -0
  153. package/src/domain/register.test.ts +153 -0
  154. package/src/domain/register.ts +261 -0
  155. package/src/domain/tex-document.ts +47 -3
  156. package/src/facts-file.test.ts +100 -4
  157. package/src/facts-file.ts +91 -9
  158. package/src/new-paper.test.ts +7 -1
  159. package/src/paper-settings.test.ts +63 -6
  160. package/src/paper-settings.ts +53 -5
  161. package/src/pdf-content.test.ts +71 -0
  162. package/src/pdf-content.ts +125 -0
  163. package/src/pdf-facts.test.ts +110 -2
  164. package/src/pdf-facts.ts +106 -8
  165. package/src/ports/latex.ts +9 -0
  166. package/src/presets.ts +15 -9
  167. package/src/register-bands.test.ts +327 -0
  168. package/src/register-bands.ts +291 -0
  169. package/src/register.ts +7 -37
  170. package/src/rule-cases.test.ts +96 -20
  171. package/src/rule-docs.test.ts +0 -2
  172. package/src/tex-requirements.ts +56 -8
  173. package/src/tex-venue-rules.test.ts +110 -5
  174. package/src/tex-venue-rules.ts +64 -3
  175. package/src/types.ts +2 -0
  176. package/src/venue-rules.test.ts +351 -39
  177. package/src/venue-rules.ts +247 -47
@@ -0,0 +1,111 @@
1
+ # anonymity/identity
2
+
3
+ **Level:** error · **Reads:** `paper.tex` → `paperlint.json`, the preset it extends, and
4
+ `_build/paper.facts.json` (what `paperlint build` read from the PDF)
5
+
6
+ ## What it catches
7
+
8
+ For a venue that reviews double-blind — its preset says `"blind": true` — two things:
9
+
10
+ - the paper declares no `identity`, so there is nothing to check the PDF against;
11
+ - a string from `identity` appears in the built PDF: on a page, in its metadata (the Info
12
+ dictionary and the XMP packet: `Author`, `Title`, `Keywords`, …), or in the target of a link.
13
+
14
+ A rule cannot know who wrote a paper, so the authors say it once, in `paperlint.json`. Given that
15
+ list, finding it is mechanical. A missing list is an error, not a pass: a check with no input and a
16
+ check that found nothing would otherwise look the same.
17
+
18
+ The PDF is what reviewers get, so the PDF is what is read — not the source. A name reached through
19
+ a macro (`\newcommand\me{Ada Example}`), a self-citation's author list from the `.bib`, a
20
+ `\hypersetup{pdfauthor=…}` that is in no page's text, a `\href` whose visible text differs from its
21
+ target: each ends up in the PDF, and each is found there.
22
+
23
+ ## Why
24
+
25
+ > Submissions must be properly anonymized for double-blind review.
26
+
27
+ — AIDC's call for papers, and most double-blind venues say the same. A paper that names its authors
28
+ is desk-rejected, and the ways a name gets in are mostly invisible when reading the PDF: metadata
29
+ the class copies from `\author`, a link to your own GitHub account, a name in a figure.
30
+
31
+ ## How a name is matched
32
+
33
+ PDF text is not the source text; the same name comes out spelled several ways, and each is matched:
34
+
35
+ | in the PDF | matched as |
36
+ | ---------------------------------------- | ---------------------------------------------------- |
37
+ | `ADA EXAMPLE` (small caps), `ада пример` | Unicode case folding, no locale — Cyrillic folds too |
38
+ | `fi` ligature | NFKC on both sides |
39
+ | `Ex` `ample` (two items, kerning) | the page's items are joined without a space |
40
+ | `A d a E x a m p l e` (letter-spaced) | whitespace may stand between any two characters |
41
+ | `Exam-` ⏎ `ple`, `Ada` ⏎ `Example` | so may a hyphen or a line break |
42
+ | `AdaExample`, `Ada-Example` | a token's own spaces and hyphens are optional |
43
+
44
+ Only the ends of a token are anchored: `Ada` does not match inside `Canada`, `MIT` not inside
45
+ `submitted`. Two tokens that find the same text in the same place are one finding.
46
+
47
+ Anonymous hosting needs no exception: an `osf.io/…?view_only=…` or `anonymous.4open.science` link
48
+ names nobody, so nothing in `identity` matches it.
49
+
50
+ ## Examples
51
+
52
+ ```json
53
+ {
54
+ "extends": "paperlint:aidc",
55
+ "kind": "regular",
56
+ "identity": [
57
+ "Ada Example",
58
+ "adaexample",
59
+ "ada@example.org",
60
+ "Example University",
61
+ "exampletool"
62
+ ]
63
+ }
64
+ ```
65
+
66
+ Findings:
67
+
68
+ > PDF, page 1: «Ada Example» matches «Ada Example» in `identity`, and aidc reviews double-blind —
69
+ > remove it, or refer to your own work in the third person
70
+
71
+ > PDF metadata: Author: «Ada Example» matches «Ada Example» in `identity`, …
72
+
73
+ > link target on page 4 (https://github.com/adaexample/exampletool): «adaexample» matches …
74
+
75
+ With no `identity`:
76
+
77
+ > aidc reviews double-blind, and this paper declares no `identity`, so nothing in the PDF can be
78
+ > checked against the authors. Declare it in paperlint.json (the root's for every paper, or this
79
+ > paper's): "identity": ["Your Name", "your-handle", "you@example.org", "Your University", "your-project"]
80
+
81
+ ## Options / preset fields
82
+
83
+ No rule options.
84
+
85
+ - `identity` in `paperlint.json` — a list of strings, each with a letter or digit. The root file's
86
+ list and the paper's own are **joined**: a paper adds its co-authors to the project's author and
87
+ cannot drop them by accident. Put in everything that identifies you: every spelling of every
88
+ author's name, handles, emails, affiliations, your own tools' and repositories' names.
89
+ - `blind` in the preset — `true` where the venue's call says the review is double-blind. Shipped:
90
+ `paperlint:aidc`. A venue that is not blind: the rule is silent, `identity` or not.
91
+
92
+ ## What it does not check
93
+
94
+ - **Text inside raster images.** A screenshot with your name, a logo, a scanned figure: text
95
+ extraction reads text, not pixels, so a name there is not seen. Look at the figures yourself.
96
+ - **What you did not declare.** A co-author missing from `identity`, a nickname, an old handle.
97
+ - **Self-citation in the third person** is allowed by most double-blind venues, and a cited name is
98
+ still a declared name: the rule reports it. Check the venue's policy; if it allows the citation,
99
+ cite in the third person and keep the finding in mind, or drop that token for this paper.
100
+ - **A name split by hyphenation inside a token** that the PDF does not mark (a hyphen is matched,
101
+ a missing space between two lines of a hyphenated compound is).
102
+ - **Words that are also names.** `identity` entries match as whole words anywhere; a common word
103
+ used as a handle will be found in ordinary prose.
104
+ - **The source.** Comments, `\iffalse` blocks and files not in the build are not read — they are
105
+ not in the PDF reviewers get. Supplementary material uploaded separately is not read either.
106
+
107
+ ## How to fix
108
+
109
+ Remove the name from where the message says: the author block, the acknowledgments (for review),
110
+ `\hypersetup{pdfauthor=…}` / `pdfusetitle`, a link to your account (use an anonymous mirror), a
111
+ self-citation in the first person. Rebuild (`paperlint build`) and lint again.
@@ -0,0 +1,61 @@
1
+ # format/layout-override
2
+
3
+ **Level:** error · **Reads:** `paper.tex` and the files it includes, and the preset its
4
+ `paperlint.json` extends
5
+
6
+ ## What it catches
7
+
8
+ A command that changes the page layout the venue's template sets for the whole document:
9
+
10
+ | command | changes |
11
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
12
+ | `\usepackage{geometry}` (alone or in a list), `\geometry{…}`, `\newgeometry{…}` | the margins |
13
+ | `\setlength` / `\addtolength` on `\textheight`, `\textwidth`, `\topmargin`, `\oddsidemargin`, `\evensidemargin`, `\columnsep`, `\headsep`, `\headheight`, `\footskip`, `\baselineskip` | the text block |
14
+ | `\linespread{…}`, `\renewcommand{\baselinestretch}{…}` | the line spacing |
15
+
16
+ It reads the parse tree, not the text: a command in a comment is not a command, and a `comment`
17
+ environment is skipped. A macro definition is read: `\newcommand{\tall}{\setlength{\textheight}{10in}}`
18
+ sets the length wherever it is used, so it is reported where it is defined.
19
+
20
+ It is silent for a paper whose `paperlint.json` names no preset, or a preset that names no
21
+ `template` — there is no mandated layout to compare against.
22
+
23
+ ## Why
24
+
25
+ Venues set the page through their template and reject papers that change it: IEEE and ACM venues
26
+ run format checks, and a shrunken margin or a tightened line spacing is a classic desk reject.
27
+ AIDC: «LaTeX submissions must use the IEEEtran.cls version 1.8b template». The page-limit and
28
+ geometry checks see the result only in the built PDF; this rule points at the line that does it.
29
+
30
+ ## Examples
31
+
32
+ ```latex
33
+ \usepackage[margin=0.7in]{geometry} % reported
34
+ \setlength{\textheight}{9.6in} % reported
35
+ \linespread{0.95} % reported
36
+ \setlength{\parindent}{0pt} % not reported: not a layout length
37
+ Text.\vspace{-2mm} % not reported: local space, not the template's layout
38
+ ```
39
+
40
+ > `\usepackage{geometry}` changes the page layout aidc's template sets
41
+ > (`\documentclass[conference,compsoc]{IEEEtran}`) — a desk-reject reason at venues that check the
42
+ > format. Remove it; if the venue allows it, disable this line with a comment saying so
43
+
44
+ ## Options / preset fields
45
+
46
+ No rule options. It reads the preset's `template`.
47
+
48
+ ## What it does not check
49
+
50
+ - Local spacing: a negative `\vspace`, `\vspace*` or `\vskip` around a float or a heading. It
51
+ changes one spot, not the layout the template sets, and no venue's call names it.
52
+ - Font size changes (`\small` body text, `\fontsize`), `\baselineskip` changed by `\fontsize`,
53
+ `\enlargethispage`, `\looseness`, or float spacing lengths (`\textfloatsep`, `\abovecaptionskip`).
54
+ - Conditionals are not evaluated: a command inside an `\iffalse … \fi` block in the preamble is
55
+ still reported.
56
+ - Layout lengths set with plain TeX assignment (`\textheight=9in`).
57
+
58
+ ## How to fix
59
+
60
+ Remove the command and let the template set the page. If the venue explicitly allows it, keep it
61
+ with `% eslint-disable-next-line format/layout-override -- <the venue's words>`.
@@ -0,0 +1,108 @@
1
+ # format/page-limit
2
+
3
+ **Level:** error · **Reads:** `paper.tex` → `paperlint.json` (the venue preset and the paper's
4
+ `kind`) and `_build/paper.facts.json` (what `paperlint build` measured in the PDF)
5
+
6
+ ## What it catches
7
+
8
+ A PDF longer than its venue allows for the paper's kind: more body pages than `body_pages_max`, or
9
+ more reference pages than `ref_pages_max`.
10
+
11
+ How the body is counted depends on the preset:
12
+
13
+ - **By default**, body and reference pages are banal's count: a page holding any text that is not
14
+ bibliography is a body page.
15
+ - **With `"body_ends_at": "references"`** (AIDC), the body is every page up to where it ends: at
16
+ the references or at the appendix, whichever comes first — «excluding references and
17
+ appendices» excludes an appendix wherever it sits. What follows is not counted. The page the body
18
+ ends on counts as body when body text stands above the heading there: a body that runs half a
19
+ page past the limit is over the limit.
20
+
21
+ ## How it finds where the body ends
22
+
23
+ Each end is found by a structural signal first, then its heading line on that page.
24
+
25
+ **The references.**
26
+
27
+ 1. **hyperref's first bibliography entry.** hyperref writes a destination named `cite.<key>` for
28
+ every `\bibitem`; the lowest page any of them is on is where the bibliography is. The build
29
+ records it as `bib_anchor_page`.
30
+ 2. **The heading.** A line that reads `References` or `Bibliography` — any case, spaces ignored
31
+ (IEEEtran prints `REFERENCES` in small capitals, which the PDF may give as `R EFERENCES`),
32
+ optionally numbered (`7 References`, `VII. REFERENCES`). A heading whose next line opens the
33
+ first entry, `[1]`, is **confirmed**.
34
+
35
+ With the anchor, the references start on its page, at the confirmed heading there, else at the last
36
+ heading line on that page. Without hyperref: at the first confirmed heading, else at the first
37
+ heading line.
38
+
39
+ **The appendix.** hyperref's destination for the appendix's first section (`appendix.A` in the
40
+ standard classes) or the first bookmark titled «Appendix…» (IEEEtran's `\appendices`) gives its
41
+ page, recorded as `appendix_anchor_page`; on that page, the line reading `Appendix A.`,
42
+ `APPENDIX` or `Appendices`. Without hyperref the appendix is not looked for: a sentence can end on
43
+ a line reading «Appendix A.», and nothing would tell it from the heading.
44
+
45
+ On the page the body ends on, the lines above the heading decide: a line with a letter in it is
46
+ body text, unless it is a running header or footer — the same line, digits aside, on at least three
47
+ pages and a third of them. A page number is not body text either.
48
+
49
+ A line reading «References» that is not the bibliography's heading — a `\subsection*{References}`,
50
+ a table column headed «References» — is not taken for it: the anchor names another page, or the
51
+ line is not followed by `[1]`. When a structural signal and the text disagree — a `cite.<key>` page
52
+ with no heading on it, a confirmed heading on another page, an appendix page with no «Appendix»
53
+ line — the count is not made, and the rule says so (`unclear`).
54
+
55
+ ## Why
56
+
57
+ AIDC limits the paper to 12 pages «excluding references and appendices», and places the LLM Usage
58
+ Statement after the references, where it «does not count towards the page limit». banal counts any
59
+ page with non-bibliography text as body, so a statement or appendix after the bibliography would
60
+ fail a correct paper. A body that runs onto the references page, even by a few lines, is a
61
+ thirteenth page — the desk-reject case this rule exists to catch.
62
+
63
+ ## Examples
64
+
65
+ A 12-page AIDC paper whose conclusion ends at the foot of page 12, «References» at the top of page
66
+ 13: body 12 pages, passes.
67
+
68
+ The same paper with the conclusion running four lines onto page 13, «References» below them:
69
+
70
+ > body pages (up to the references on page 13): 13, over the limit 12 for aidc/regular — a desk
71
+ > reject; cut the text
72
+
73
+ An accepted ACSAC paper that places its appendix tables before the bibliography: the appendix starts
74
+ at the top of page 12, the references on page 15. The body is 11 pages.
75
+
76
+ When a structural signal and the text disagree, the count is not made and the rule says so:
77
+
78
+ > could not tell where the body ends, so it was NOT counted against aidc's limit: the first
79
+ > bibliography entry is on page 14 (hyperref's destination), and no line on page 14 reads
80
+ > «References». Check the headings of the bibliography and the appendix, and that nothing before
81
+ > them reads «References» above a [1]
82
+
83
+ ## Options / preset fields
84
+
85
+ No rule options. It reads, from the preset, `format.kinds.<kind>.body_pages_max`,
86
+ `format.kinds.<kind>.ref_pages_max` and `format.body_ends_at`; from the paper's `paperlint.json`,
87
+ `kind`. A preset with no kinds, or a kind with no limits, has nothing to check.
88
+
89
+ ## What it does not check
90
+
91
+ - **An appendix before the references, without hyperref**, is counted as body: nothing structural
92
+ says where it starts. With hyperref it ends the body.
93
+ - **An appendix whose heading does not read «Appendix…»** (the standard classes print `A` and the
94
+ title) is reported as `unclear` when hyperref anchors it.
95
+ - **A bibliography that is not numbered and not linked by hyperref** has no confirmed heading and no
96
+ anchor; the first line reading «References» is then taken, and a body line reading exactly that
97
+ would be taken for it.
98
+ - **A running header that repeats on fewer than three pages** (a two-page paper) counts as body
99
+ text above a heading at the top of a page.
100
+ - Whether the venue counts pages the same way for other kinds of material (supplementary files,
101
+ artifact appendices).
102
+
103
+ ## How to fix
104
+
105
+ Cut the body until it ends within the limit. With `body_ends_at`, check where the body ends on the
106
+ rendered PDF: body text above the heading on that page counts that page. For `unclear`, give the
107
+ bibliography its heading (`\bibliography`, `thebibliography`) and the appendix one reading
108
+ «Appendix», and make sure no line before them reads «References» above a `[1]`.
@@ -0,0 +1,87 @@
1
+ # pdf/body-size
2
+
3
+ **Level:** error · **Reads:** `paper.tex` → `paperlint.json` (the venue preset) and
4
+ `_build/paper.facts.json` (what `paperlint build` measured in the PDF)
5
+
6
+ ## What it catches
7
+
8
+ A PDF set in another size than its venue's template:
9
+
10
+ - the body font size more than `body_pt_tol` off the preset's `body_pt` (`body`);
11
+ - the reference font size outside the preset's `ref_pt_min`–`ref_pt_max`, widened by `body_pt_tol`
12
+ on both sides (`refPt`);
13
+ - a build that measured no body size at all (`bodyMissing`) — rebuild.
14
+
15
+ Both sizes are what banal measures on the rendered pages: the most common size of the body text,
16
+ and of the first bibliography page. It is not the size the source declares, so a
17
+ `\documentclass[9pt]`, a `\fontsize` in the preamble or a bibliography wrapped in `\scriptsize` are
18
+ all seen, whatever macro did it.
19
+
20
+ ## Why
21
+
22
+ A call for papers that fixes a template fixes its font size, and shrinking the text is the usual
23
+ way a paper is made to fit the page limit. A reviewer or a production editor who notices returns
24
+ the paper; a page limit checked on a shrunk paper checks nothing.
25
+
26
+ It is an error because it was measured to be exact on real papers, not assumed. On five accepted
27
+ papers, built (`fixtures/accepted-papers/body-size.json`, `test/e2e/tex/page-count.e2e.ts`):
28
+
29
+ | paper | declared | rendered (pdf.js) | measured (banal) | finding |
30
+ | ------------------------------ | ---------------------- | ------------------------------------------ | ---------------- | ------- |
31
+ | four ACSAC papers, IEEEtran | 10 pt, references 8 pt | 10.00 / 8.00 (9.96 / 7.97 without compsoc) | 10.3 / 8.3 | none |
32
+ | AgenticDev '26, acmart sigconf | 9 pt, references 7 pt | 8.97 / 6.97 | 9.3 / 7.3 | none |
33
+ | an ACSAC paper with `9pt` | 9 pt | 9.00 / 8.00 | 9.3 / 8.3 | `body` |
34
+ | references in `\scriptsize` | 7 pt | 10.00 / 7.00 | 10.3 / 7.3 | `refPt` |
35
+ | references in `\small` | 9 pt | 9.96 / 8.97 | 10.3 / 9.3 | `refPt` |
36
+
37
+ banal reads every size 0.3 pt above the size the glyphs are set in, on both templates, and
38
+ `body_pt_tol` of 0.5 absorbs that. Because of that offset the margins are not symmetric: every accepted body, and every IEEE bibliography (a single size, 8 pt), lands 0.2 pt inside the upper edge, a paper one size smaller (9 pt body, 7 pt
39
+ references) 0.2 pt outside the lower edge, and one size larger 0.8 pt outside the upper one. The
40
+ offset was exactly 0.3 on all eight builds; a template on which banal reads differently would need
41
+ its own measurement. A change of half a point (IEEEtran's 9pt `\small` is 8.5 pt) can pass
42
+ unreported.
43
+
44
+ ## Examples
45
+
46
+ An IEEEtran paper built with `\documentclass[9pt,conference,compsoc]{IEEEtran}`:
47
+
48
+ > the body font size measures 9.3 pt against 10 ± 0.5 pt for ieee-conference. The measurement is
49
+ > the mode of the rendered text, not the declared size — check \documentclass and its options
50
+
51
+ The same paper with `{\let\footnotesize\scriptsize\bibliography{refs}}`:
52
+
53
+ > the reference font size is 7.3 pt, outside 8 pt for ieee-conference (allowing ±0.5 pt for the
54
+ > measuring drift) — fix the bibliography's font size
55
+
56
+ The paper as the template sets it, `\documentclass[conference,compsoc]{IEEEtran}` and a plain
57
+ `\bibliography{refs}`, passes.
58
+
59
+ ## Options / preset fields
60
+
61
+ No rule options. From the preset's `format`: `body_pt` and `body_pt_tol` (both needed for the body
62
+ check), `ref_pt_min` and `ref_pt_max` (both needed for the reference check). A preset without them
63
+ does not check that size. The shipped presets:
64
+
65
+ | preset | `body_pt` ± `body_pt_tol` | references |
66
+ | ----------------------------------------- | ------------------------- | ---------- |
67
+ | `ieee-conference` (and `aidc`) | 10 ± 0.5 | 8 |
68
+ | `acm-sigconf` (and `agenticdev`, `aisec`) | 9 ± 0.5 | 7–9 |
69
+ | `realm` | 11 ± 0.5 | 9–11 |
70
+
71
+ ## What it does not check
72
+
73
+ - **The size of anything but the body and the first bibliography page**: captions, footnotes,
74
+ tables, the title and the appendix are not measured. A table set in `\tiny` passes.
75
+ - **A paper that is not built.** Without `_build/paper.facts.json`, or without banal, the rule is
76
+ silent and `pdf/measured` says why.
77
+ - **The `realm` preset** has not been checked on built accepted papers of its venue: its body size
78
+ is ACL's stated 11 pt, and its reference range was set from a single measurement.
79
+ - A paper with no bibliography has no reference size, and only the body is judged.
80
+
81
+ ## How to fix
82
+
83
+ Use the template's size: remove the size option from `\documentclass` and any `\fontsize`,
84
+ `\small` or `\footnotesize` around the body; for the references, remove what wraps
85
+ `\bibliography` or changes `\bibfont`. Rebuild (`paperlint build`) and lint again. If the venue
86
+ really allows another size, override the preset's `format` in your own preset, or set the rule to
87
+ `"off"` in `paperlint.json`.
@@ -0,0 +1,71 @@
1
+ # tex/claim-emphasis
2
+
3
+ **Level:** warn · **Reads:** `paper.tex` and the files it includes → `paperlint.json`
4
+
5
+ ## What it catches
6
+
7
+ A body that sets its claims in bold inside running sentences more often than the accepted papers of
8
+ its venue do. A claim in bold is a `\textbf{…}` (or `{\bfseries …}`, `{\bf …}`) that:
9
+
10
+ - stands **inside** a sentence — not opening a paragraph or a list item, where bold is a label
11
+ (`\textbf{Varying the volume}. We …`, `\item \textbf{A corpus of questions}: …`), braces around it
12
+ or not; and
13
+ - **states** rather than names: six words or more, or holding a number
14
+ (`\textbf{median coverage is 2.5/10}`, `\textbf{they react to the dangerous word}`).
15
+
16
+ A term in bold (`\textbf{Pressure-Acoustic Transformation (PAT)}`), italics and `\emph`, headings,
17
+ run-in headings, captions, tables and figures are not counted. Counted per 10,000 words of the body.
18
+
19
+ Above the band it reports once at the start of the body, then once at every claim in bold, where it
20
+ stands. Below the band it reports once. A body under 2 000 words, or a paper whose preset records no
21
+ anchors, is not judged.
22
+
23
+ ## Why
24
+
25
+ Setting a finding in bold is how a post makes a point land; an accepted paper puts the number in a
26
+ sentence that says whose it is, or in a table. Measured on the LaTeX source, per 10,000 words:
27
+
28
+ | paper | words | claims in bold | per 10,000 |
29
+ | ------------------------------------------------------- | ----: | -------------: | ---------: |
30
+ | `leaking-queries-acsac25` (ACSAC 2025) | 9133 | 0 | 0.0 |
31
+ | `rr-dataset-quality-acsac24` (ACSAC 2024) | 8130 | 0 | 0.0 |
32
+ | `secure-acsac24` (ACSAC 2024) | 7519 | 0 | 0.0 |
33
+ | `llm-splained-acsac25` (ACSAC 2025) | 9529 | 1 | 1.0 |
34
+ | `barovox-acsac24` (ACSAC 2024) | 7670 | 8 | 10.4 |
35
+ | the paper reviewers called a blog post, current version | 10476 | 42 | 40.1 |
36
+ | the same paper, before its rewrite | 6128 | 42 | 68.5 |
37
+ | `agenticdev-acm26` (ours, "too informal", ACM) | 4374 | 8 | 18.3 |
38
+
39
+ Two shapes were measured and left out, because they do not separate the groups: all bold inside a
40
+ sentence (accepted papers 0–44 per 10,000, that paper 60 — terms in bold are common), and
41
+ phrases of six words or more in italics (accepted 0–7.8, the paper 4.8 — italics carry quotes and
42
+ examples). BaroVox's 8 are six of its results set in bold (_0.35_, _4.09/5_, …), its claim of a
43
+ _first-ever use of this technique on pressure sensors_, and a dataset name with a digit
44
+ (_SpeechCommand256_). That name and LLM-splained's one count, a list of topics in bold, are the two
45
+ false positives in the corpus; the band keeps both papers inside.
46
+
47
+ ## Examples
48
+
49
+ ```latex
50
+ We find that \textbf{the median guard blocks 2 of 10 disasters}. % a claim in bold
51
+ The median guard blocks 2 of 10 disasters (Table~\ref{t:cov}). % none
52
+ We introduce \textbf{GateBench}, a harness for scraped guards. % none: a term
53
+ \textbf{Threat model.} The attacker controls the prompt. % none: a run-in heading
54
+ ```
55
+
56
+ ## Options / preset fields
57
+
58
+ No rule options. The band is the venue preset's `register.anchors`, exactly as for
59
+ [`tex/contrast-frames`](contrast-frames.md), which explains how it is derived. For `paperlint:aidc` it
60
+ is **0–17.8**.
61
+
62
+ ## What it does not check
63
+
64
+ Whether a bold phrase deserves emphasis, a claim in bold shorter than six words with no number
65
+ (_it is unsafe_), or emphasis by other means (capitals, colour, a box).
66
+
67
+ ## How to fix
68
+
69
+ Take the bold off a finding and let the sentence carry it — with its owner (_we measured_, a
70
+ `\ref` to the table) — or move the number into a table or figure. Keep bold for a term at its
71
+ definition and for a label that opens a paragraph.
@@ -0,0 +1,100 @@
1
+ # tex/contrast-frames
2
+
3
+ **Level:** warn · **Reads:** `paper.tex` and the files it includes → `paperlint.json`
4
+
5
+ ## What it catches
6
+
7
+ A body whose contrast frames — a claim set against a rejected alternative — run outside the band
8
+ the accepted papers of its venue set. Counted per 10,000 words of the body, in these forms:
9
+
10
+ | form | example |
11
+ | ------------------------------- | --------------------------------------------------------------- |
12
+ | _X, not Y_ (and `;`, `---`) | _they react to the dangerous word, not the dangerous operation_ |
13
+ | _X, and not Y_ · _X, but not Y_ | _it reads the field, and not the raw input_ |
14
+ | _not X but Y_ (one clause) | _over-blocking is not a side metric but a precondition_ |
15
+ | _rather than_ | _it fires on the mention rather than the act_ |
16
+ | _instead of_ | _it reads the whole event instead of the command_ |
17
+ | _as opposed to_ | _it matches words as opposed to operations_ |
18
+
19
+ Not counted: _not only … but also_; a negated clause followed by _, but_ (_the audit is not
20
+ reported, but …_); _, not yet_ · _not already_ · _not necessarily_ and their kin, which reject
21
+ nothing; _without_ and _unlike_ — both measured, both as frequent in accepted papers as in the
22
+ paper reviewers called a blog post, so they would add noise and no signal.
23
+
24
+ Above the band it reports once at the start of the body — the count, the rate, the band and the
25
+ anchors' own range, with the first three sentences — and then once at every frame, with the words
26
+ around it, so an editor shows where to rewrite. Below the band it reports once. A body under 2 000
27
+ words is not judged, and neither is a paper whose preset records no anchors.
28
+
29
+ It reads the same body as [`tex/register`](register.md): from the start of the document, the
30
+ abstract included, to `\appendix` or the bibliography, includes spliced in; headings, run-in
31
+ headings, captions, tables, figures and code are not prose.
32
+
33
+ ## Why
34
+
35
+ Reviewers called a paper "too informal" and "a blogpost". Its body set a claim against what it is
36
+ not several times per page — the device carries the argument in place of a word that names the
37
+ relation (_because_, _therefore_). Measured on the LaTeX source, per 10,000 words:
38
+
39
+ | paper | words | frames | per 10,000 |
40
+ | ---------------------------------------------------------- | ----: | -----: | ---------: |
41
+ | `barovox-acsac24` (ACSAC 2024) | 7670 | 1 | 1.3 |
42
+ | `rr-dataset-quality-acsac24` (ACSAC 2024) | 8130 | 3 | 3.7 |
43
+ | `secure-acsac24` (ACSAC 2024) | 7519 | 3 | 4.0 |
44
+ | `llm-splained-acsac25` (ACSAC 2025) | 9529 | 4 | 4.2 |
45
+ | `leaking-queries-acsac25` (ACSAC 2025) | 9133 | 5 | 5.5 |
46
+ | the paper reviewers called a blog post, before its rewrite | 6128 | 62 | 101.2 |
47
+ | the same paper, after a rewrite aimed at contrast frames | 6121 | 25 | 40.8 |
48
+ | the same paper, its current version | 10476 | 29 | 27.7 |
49
+ | `agenticdev-acm26` (ours, "too informal", ACM) | 4374 | 22 | 50.3 |
50
+
51
+ An independent blind hand count — a reader who did not know which text was which, counting every
52
+ explicit negation that rejects one named term for another, _without_ and _unlike_ included — put the
53
+ first version at ~86 per 10,000 words and four accepted ACSAC excerpts at ~9–19: a different counter,
54
+ the same five- to tenfold gap. Compare rates only within one counter.
55
+
56
+ ## Examples
57
+
58
+ ```latex
59
+ The guards react to the dangerous word, not the dangerous operation. % a frame
60
+ The guards match the words of a command; they do not parse the operation. % none
61
+ Because the guards match words, 24 of 46 block a harmless echo. % none
62
+ ```
63
+
64
+ ## Options / preset fields
65
+
66
+ No rule options. The band is the preset's: `register.anchors` in the venue preset, each anchor a
67
+ paper with its body's `words` and its count of each measure, as these rules count them:
68
+
69
+ ```jsonc
70
+ "register": {
71
+ "anchors": [
72
+ { "paper": "barovox-acsac24", "words": 7670, "contrast_frames": 1, "claim_emphasis": 8, "relation_markers": 31 }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ The band is the anchors' range widened at each end by two Poisson standard deviations of the anchor
78
+ standing there (an anchor with no occurrence is widened as if it had one; below zero is zero), so a
79
+ paper of that anchor's length and register falls outside it about one time in forty. For
80
+ `paperlint:aidc`, the anchors are the five accepted ACSAC papers above — AIDC has no accepted papers of
81
+ its own yet, and uses ACSAC's template — and the band is **0–10.4**. `src/register-bands.test.ts`
82
+ re-measures every anchor that has a fixture, so a change to a counter changes the preset with it.
83
+
84
+ A preset of your own gets a band the same way: measure accepted papers of the venue, as LaTeX
85
+ source, and record them (`anchorOf` in `src/register-bands.ts` is the one way an anchor is measured).
86
+
87
+ ## What it does not check
88
+
89
+ Whether a given frame is needed. A frame is the right sentence where a reader would otherwise assume
90
+ the rejected alternative; the rule counts, and judging each one is the job of the `grade-paper-writing`
91
+ skill. It does not read a frame split over two sentences (_It is fast. Not exact._), a negation by
92
+ synonym (_ignores_, _fails to_), or anything in the appendix.
93
+
94
+ ## How to fix
95
+
96
+ Say what the thing is and why, with the evidence, and name the relation to the sentence before —
97
+ _because_, _therefore_, _for example_ — instead of setting it against what it is not. Replacing
98
+ _, not_ with _rather than_ changes nothing: both are counted. Keep a frame where the rejected reading
99
+ is the one a reviewer brings to the page, and check after each rewrite that every claim and number
100
+ still says what it said.
@@ -0,0 +1,72 @@
1
+ # tex/relation-markers
2
+
3
+ **Level:** warn · **Reads:** `paper.tex` and the files it includes → `paperlint.json`
4
+
5
+ ## What it catches
6
+
7
+ A body that names how its sentences relate less often — or far more often — than the accepted papers
8
+ of its venue. A relation marker is one of: _therefore_, _thus_, _hence_, _consequently_, _as a
9
+ result_, _because_, _since_, _this means_, _this implies_, _which explains_, _in turn_, _that is,_,
10
+ _i.e._, _in contrast_, _by contrast_, _however_, _nevertheless_, _nonetheless_, _whereas_, _in other
11
+ words_, _for this reason_, _it follows_, _accordingly_, _specifically,_, _in particular,_, _for
12
+ example_, _for instance_, _e.g._ — as whole words, anywhere in a sentence. Counted per 10,000 words of
13
+ the body.
14
+
15
+ Outside the band it reports once, at the start of the body, with the count, the rate, the band and
16
+ the anchors' range. A body under 2 000 words, or a paper whose preset records no anchors, is not
17
+ judged.
18
+
19
+ This is the FLOOR among the register checks: [`tex/contrast-frames`](contrast-frames.md) and
20
+ [`tex/claim-emphasis`](claim-emphasis.md) catch a device used too often; this one catches the
21
+ relation left for the reader to infer, which is what a rewrite that only deletes devices leaves
22
+ behind.
23
+
24
+ ## Why
25
+
26
+ A reviewer wrote: _"Readers are often required to infer the intended relationships between claims."_
27
+ Measured on the LaTeX source, per 10,000 words:
28
+
29
+ | paper | words | markers | per 10,000 |
30
+ | ------------------------------------------------------- | ----: | ------: | ---------: |
31
+ | `rr-dataset-quality-acsac24` (ACSAC 2024) | 8130 | 32 | 39.4 |
32
+ | `barovox-acsac24` (ACSAC 2024) | 7670 | 31 | 40.4 |
33
+ | `secure-acsac24` (ACSAC 2024) | 7519 | 38 | 50.5 |
34
+ | `llm-splained-acsac25` (ACSAC 2025) | 9529 | 84 | 88.2 |
35
+ | `leaking-queries-acsac25` (ACSAC 2025) | 9133 | 103 | 112.8 |
36
+ | the paper reviewers called a blog post, current version | 10476 | 20 | 19.1 |
37
+ | the same paper, before its rewrite | 6128 | 9 | 14.7 |
38
+ | `agenticdev-acm26` (ours, "too informal", ACM) | 4374 | 13 | 29.7 |
39
+
40
+ _However_ alone stands 6–11 times in every accepted paper and never in the current version of ours. The
41
+ separation survives dropping any one marker from the list: with each of the 24 that occur removed in
42
+ turn, the lowest accepted paper stays at least 1.38 times the highest version of ours (the worst case
43
+ is dropping _however_). The list was fixed before this
44
+ corpus was measured (it is the one a register diagnosis of the same papers used, on text extracted
45
+ from PDFs, where the gap was narrow); on the parse tree the gap is twofold.
46
+
47
+ ## Examples
48
+
49
+ ```latex
50
+ Most guards match words. 24 of 46 block a harmless echo. % the relation is left to infer
51
+ Because most guards match words, 24 of 46 block a harmless echo. % named
52
+ Most guards match words; as a result, 24 of 46 block a harmless echo. % named
53
+ ```
54
+
55
+ ## Options / preset fields
56
+
57
+ No rule options. The band is the venue preset's `register.anchors`, exactly as for
58
+ [`tex/contrast-frames`](contrast-frames.md), which explains how it is derived. For `paperlint:aidc` it
59
+ is **25.4–135.0**.
60
+
61
+ ## What it does not check
62
+
63
+ Whether a marker names the right relation, or a relation named another way (_this is why_, _so that_,
64
+ a colon). The list counts only words whose meaning does not depend on context; _since_ is on it and
65
+ is sometimes temporal.
66
+
67
+ ## How to fix
68
+
69
+ Where two sentences stand side by side, say whether the second is the cause, the consequence, an
70
+ example or a limit of the first — with the word, not with a dash, a colon or a short sentence left to
71
+ land the point. The fix adds words; pay for them by moving detail to a table or the appendix, not by
72
+ cutting the connectives from another paragraph.